v0.3.7 - working on cinfig api
This commit is contained in:
286
api/index.html
286
api/index.html
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
border-bottom: 2px solid black;
|
border-bottom: 2px solid black;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 30px 0 15px 0;
|
margin: 30px 0 15px 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -36,25 +37,28 @@
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea, select, button {
|
input,
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
@@ -63,7 +67,7 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -71,65 +75,65 @@
|
|||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
color: #666;
|
color: #666;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.connected {
|
.status.connected {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.disconnected {
|
.status.disconnected {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.authenticated {
|
.status.authenticated {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.error {
|
.status.error {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-table {
|
.config-table {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-table th,
|
.config-table th,
|
||||||
.config-table td {
|
.config-table td {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-table th {
|
.config-table th {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-display {
|
.json-display {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
@@ -141,7 +145,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-panel {
|
.log-panel {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -150,71 +154,72 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-entry {
|
.log-entry {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-timestamp {
|
.log-timestamp {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-buttons {
|
.inline-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-buttons button {
|
.inline-buttons button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-pubkey {
|
.user-pubkey {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login-section {
|
#login-section {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
body {
|
body {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-buttons {
|
.inline-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>C-RELAY ADMIN API</h1>
|
<h1>C-RELAY ADMIN API</h1>
|
||||||
|
|
||||||
<!-- Testing Section - Always Visible -->
|
<!-- Testing Section - Always Visible -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>DEBUG - TEST FETCH WITHOUT LOGIN</h2>
|
<h2>DEBUG - TEST FETCH WITHOUT LOGIN</h2>
|
||||||
@@ -225,7 +230,7 @@
|
|||||||
<div class="status disconnected" id="relay-status">READY TO FETCH</div>
|
<div class="status disconnected" id="relay-status">READY TO FETCH</div>
|
||||||
<button type="button" id="fetch-config-btn">FETCH CONFIGURATION (NO LOGIN)</button>
|
<button type="button" id="fetch-config-btn">FETCH CONFIGURATION (NO LOGIN)</button>
|
||||||
<div class="status disconnected" id="config-status">NO CONFIGURATION LOADED</div>
|
<div class="status disconnected" id="config-status">NO CONFIGURATION LOADED</div>
|
||||||
|
|
||||||
<div id="config-display" class="hidden">
|
<div id="config-display" class="hidden">
|
||||||
<div id="config-view-mode">
|
<div id="config-view-mode">
|
||||||
<table class="config-table" id="config-table">
|
<table class="config-table" id="config-table">
|
||||||
@@ -239,7 +244,7 @@
|
|||||||
<tbody id="config-table-body">
|
<tbody id="config-table-body">
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="inline-buttons">
|
<div class="inline-buttons">
|
||||||
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
|
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
|
||||||
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
|
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
|
||||||
@@ -251,13 +256,13 @@
|
|||||||
<div id="config-form" class="section">
|
<div id="config-form" class="section">
|
||||||
<!-- Dynamic form will be generated here -->
|
<!-- Dynamic form will be generated here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inline-buttons">
|
<div class="inline-buttons">
|
||||||
<button type="button" id="save-config-btn">SAVE & PUBLISH</button>
|
<button type="button" id="save-config-btn">SAVE & PUBLISH</button>
|
||||||
<button type="button" id="cancel-edit-btn">CANCEL</button>
|
<button type="button" id="cancel-edit-btn">CANCEL</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="config-raw-display">
|
<div id="config-raw-display">
|
||||||
<h3>Raw Event JSON:</h3>
|
<h3>Raw Event JSON:</h3>
|
||||||
<div class="json-display" id="raw-config-json"></div>
|
<div class="json-display" id="raw-config-json"></div>
|
||||||
@@ -273,21 +278,23 @@
|
|||||||
<!-- nostr-lite login UI will be injected here -->
|
<!-- nostr-lite login UI will be injected here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Interface (hidden until logged in) -->
|
<!-- Main Interface (hidden until logged in) -->
|
||||||
<div id="main-interface" class="hidden">
|
<div id="main-interface" class="hidden">
|
||||||
|
|
||||||
<!-- User Info Section -->
|
<!-- User Info Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>LOGGED IN USER</h2>
|
<h2>LOGGED IN USER</h2>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div><strong>Name:</strong> <span id="user-name">Loading...</span></div>
|
<div><strong>Name:</strong> <span id="user-name">Loading...</span></div>
|
||||||
<div><strong>Public Key:</strong> <div class="user-pubkey" id="user-pubkey">Loading...</div></div>
|
<div><strong>Public Key:</strong>
|
||||||
|
<div class="user-pubkey" id="user-pubkey">Loading...</div>
|
||||||
|
</div>
|
||||||
<div><strong>About:</strong> <span id="user-about">Loading...</span></div>
|
<div><strong>About:</strong> <span id="user-about">Loading...</span></div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="logout-btn">LOGOUT</button>
|
<button type="button" id="logout-btn">LOGOUT</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Command Section -->
|
<!-- Command Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>ADMIN COMMANDS</h2>
|
<h2>ADMIN COMMANDS</h2>
|
||||||
@@ -300,20 +307,20 @@
|
|||||||
<option value="get_status">Get Status</option>
|
<option value="get_status">Get Status</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="command-payload">Command Payload (JSON):</label>
|
<label for="command-payload">Command Payload (JSON):</label>
|
||||||
<textarea id="command-payload" rows="4" placeholder='{"param": "value"}'></textarea>
|
<textarea id="command-payload" rows="4" placeholder='{"param": "value"}'></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label>Event Preview:</label>
|
<label>Event Preview:</label>
|
||||||
<div class="json-display" id="event-preview">No event constructed</div>
|
<div class="json-display" id="event-preview">No event constructed</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" id="send-command-btn">SEND COMMAND</button>
|
<button type="button" id="send-command-btn">SEND COMMAND</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Log Section -->
|
<!-- Log Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>EVENT LOG</h2>
|
<h2>EVENT LOG</h2>
|
||||||
@@ -324,7 +331,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" id="clear-log-btn">CLEAR LOG</button>
|
<button type="button" id="clear-log-btn">CLEAR LOG</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Load the official nostr-tools bundle first -->
|
<!-- Load the official nostr-tools bundle first -->
|
||||||
@@ -342,10 +349,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Global error handler to prevent page refreshes
|
// Global error handler to prevent page refreshes
|
||||||
window.addEventListener('error', function(e) {
|
window.addEventListener('error', function (e) {
|
||||||
console.error('Global error caught:', e.error);
|
console.error('Global error caught:', e.error);
|
||||||
console.error('Error message:', e.message);
|
console.error('Error message:', e.message);
|
||||||
console.error('Error filename:', e.filename);
|
console.error('Error filename:', e.filename);
|
||||||
@@ -354,7 +361,7 @@
|
|||||||
return true; // Prevent page refresh
|
return true; // Prevent page refresh
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', function(e) {
|
window.addEventListener('unhandledrejection', function (e) {
|
||||||
console.error('Unhandled promise rejection:', e.reason);
|
console.error('Unhandled promise rejection:', e.reason);
|
||||||
e.preventDefault(); // Prevent default browser error handling
|
e.preventDefault(); // Prevent default browser error handling
|
||||||
return true; // Prevent page refresh
|
return true; // Prevent page refresh
|
||||||
@@ -399,10 +406,10 @@
|
|||||||
function log(message, type = 'INFO') {
|
function log(message, type = 'INFO') {
|
||||||
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
||||||
const logMessage = `${timestamp} [${type}]: ${message}`;
|
const logMessage = `${timestamp} [${type}]: ${message}`;
|
||||||
|
|
||||||
// Always log to browser console so we don't lose logs on refresh
|
// Always log to browser console so we don't lose logs on refresh
|
||||||
console.log(logMessage);
|
console.log(logMessage);
|
||||||
|
|
||||||
// Also log to UI if elements exist
|
// Also log to UI if elements exist
|
||||||
if (logPanel) {
|
if (logPanel) {
|
||||||
const logEntry = document.createElement('div');
|
const logEntry = document.createElement('div');
|
||||||
@@ -418,35 +425,33 @@
|
|||||||
try {
|
try {
|
||||||
await window.NOSTR_LOGIN_LITE.init({
|
await window.NOSTR_LOGIN_LITE.init({
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
darkMode: false,
|
|
||||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
||||||
methods: {
|
methods: {
|
||||||
extension: true,
|
extension: true,
|
||||||
local: true,
|
local: true,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
connect: true, // Enables "Nostr Connect" (NIP-46)
|
connect: true,
|
||||||
remote: true, // Also needed for "Nostr Connect" compatibility
|
remote: true,
|
||||||
otp: true // Enables "DM/OTP"
|
otp: true
|
||||||
},
|
},
|
||||||
floatingTab: {
|
floatingTab: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
hPosition: 0.98, // 80% from left
|
hPosition: 1, // 0.0-1.0 or '95%' from left
|
||||||
vPosition: 0.00, // Top of page
|
vPosition: 0, // 0.0-1.0 or '50%' from top
|
||||||
appearance: {
|
appearance: {
|
||||||
style: 'minimal',
|
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
||||||
theme: 'auto',
|
icon: '', // Clean display without icon placeholders
|
||||||
icon: '',
|
text: 'Login'
|
||||||
text: 'Login',
|
|
||||||
iconOnly: false
|
|
||||||
},
|
},
|
||||||
behavior: {
|
behavior: {
|
||||||
hideWhenAuthenticated: false,
|
hideWhenAuthenticated: false,
|
||||||
showUserInfo: true,
|
showUserInfo: true,
|
||||||
autoSlide: false,
|
autoSlide: true
|
||||||
persistent: false
|
},
|
||||||
}
|
getUserInfo: true, // Enable profile fetching
|
||||||
},
|
getUserRelay: [ // Specific relays for profile fetching
|
||||||
debug: true
|
'wss://relay.laantungir.net'
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
nlLite = window.NOSTR_LOGIN_LITE;
|
nlLite = window.NOSTR_LOGIN_LITE;
|
||||||
@@ -462,26 +467,26 @@
|
|||||||
|
|
||||||
// Handle authentication events
|
// Handle authentication events
|
||||||
function handleAuthEvent(event) {
|
function handleAuthEvent(event) {
|
||||||
const {pubkey, method, error} = event.detail;
|
const { pubkey, method, error } = event.detail;
|
||||||
|
|
||||||
if (method && pubkey) {
|
if (method && pubkey) {
|
||||||
userPubkey = pubkey;
|
userPubkey = pubkey;
|
||||||
isLoggedIn = true;
|
isLoggedIn = true;
|
||||||
console.log(`Login successful! Method: ${method}`);
|
console.log(`Login successful! Method: ${method}`);
|
||||||
console.log(`Public key: ${pubkey}`);
|
console.log(`Public key: ${pubkey}`);
|
||||||
|
|
||||||
showMainInterface();
|
showMainInterface();
|
||||||
loadUserProfile();
|
loadUserProfile();
|
||||||
|
|
||||||
// Automatically fetch configuration after login
|
// Automatically fetch configuration after login
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fetchConfiguration().catch(error => {
|
fetchConfiguration().catch(error => {
|
||||||
console.log('Auto-fetch configuration failed: ' + error.message);
|
console.log('Auto-fetch configuration failed: ' + error.message);
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
console.log('Login successful. Auto-fetching configuration...');
|
console.log('Login successful. Auto-fetching configuration...');
|
||||||
|
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
console.log(`Authentication error: ${error}`);
|
console.log(`Authentication error: ${error}`);
|
||||||
}
|
}
|
||||||
@@ -506,7 +511,7 @@
|
|||||||
// Create a SimplePool instance
|
// Create a SimplePool instance
|
||||||
relayPool = new window.NostrTools.SimplePool();
|
relayPool = new window.NostrTools.SimplePool();
|
||||||
const relays = ['wss://relay.laantungir.net'];
|
const relays = ['wss://relay.laantungir.net'];
|
||||||
|
|
||||||
// Get profile event (kind 0) for the user
|
// Get profile event (kind 0) for the user
|
||||||
const events = await relayPool.querySync(relays, {
|
const events = await relayPool.querySync(relays, {
|
||||||
kinds: [0],
|
kinds: [0],
|
||||||
@@ -549,7 +554,7 @@
|
|||||||
const relays = ['wss://relay.laantungir.net'];
|
const relays = ['wss://relay.laantungir.net'];
|
||||||
relayPool = null;
|
relayPool = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up configuration WebSocket
|
// Clean up configuration WebSocket
|
||||||
if (configWebSocket) {
|
if (configWebSocket) {
|
||||||
console.log('Closing configuration WebSocket...');
|
console.log('Closing configuration WebSocket...');
|
||||||
@@ -557,22 +562,22 @@
|
|||||||
configWebSocket = null;
|
configWebSocket = null;
|
||||||
subscriptionId = null;
|
subscriptionId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await nlLite.logout();
|
await nlLite.logout();
|
||||||
|
|
||||||
userPubkey = null;
|
userPubkey = null;
|
||||||
isLoggedIn = false;
|
isLoggedIn = false;
|
||||||
currentConfig = null;
|
currentConfig = null;
|
||||||
|
|
||||||
// Reset UI
|
// Reset UI
|
||||||
mainInterface.classList.add('hidden');
|
mainInterface.classList.add('hidden');
|
||||||
loginSection.classList.remove('hidden');
|
loginSection.classList.remove('hidden');
|
||||||
updateConfigStatus(false);
|
updateConfigStatus(false);
|
||||||
relayStatus.textContent = 'READY TO FETCH';
|
relayStatus.textContent = 'READY TO FETCH';
|
||||||
relayStatus.className = 'status disconnected';
|
relayStatus.className = 'status disconnected';
|
||||||
|
|
||||||
console.log('Logged out successfully');
|
console.log('Logged out successfully');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Logout failed: ' + error.message);
|
console.log('Logout failed: ' + error.message);
|
||||||
}
|
}
|
||||||
@@ -603,11 +608,11 @@
|
|||||||
async function subscribeToConfiguration() {
|
async function subscribeToConfiguration() {
|
||||||
try {
|
try {
|
||||||
console.log('=== STARTING DIRECT WEBSOCKET CONFIGURATION SUBSCRIPTION ===');
|
console.log('=== STARTING DIRECT WEBSOCKET CONFIGURATION SUBSCRIPTION ===');
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
console.log('WARNING: Not logged in, but proceeding with subscription test');
|
console.log('WARNING: Not logged in, but proceeding with subscription test');
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = relayUrl.value.trim();
|
const url = relayUrl.value.trim();
|
||||||
if (!url) {
|
if (!url) {
|
||||||
console.error('Please enter a relay URL');
|
console.error('Please enter a relay URL');
|
||||||
@@ -653,10 +658,10 @@
|
|||||||
reject(new Error('Connection timeout'));
|
reject(new Error('Connection timeout'));
|
||||||
}, 10000); // 10 second timeout
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
configWebSocket.onopen = function(event) {
|
configWebSocket.onopen = function (event) {
|
||||||
console.log('WebSocket connection established');
|
console.log('WebSocket connection established');
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
|
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
|
||||||
relayStatus.className = 'status connected';
|
relayStatus.className = 'status connected';
|
||||||
|
|
||||||
@@ -683,7 +688,7 @@
|
|||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
configWebSocket.onmessage = function(event) {
|
configWebSocket.onmessage = function (event) {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
console.log('Received message:', message);
|
console.log('Received message:', message);
|
||||||
@@ -704,12 +709,12 @@
|
|||||||
|
|
||||||
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
||||||
relayStatus.className = 'status connected';
|
relayStatus.className = 'status connected';
|
||||||
|
|
||||||
} else if (messageType === "EOSE" && subId === subscriptionId) {
|
} else if (messageType === "EOSE" && subId === subscriptionId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.log('EOSE received - End of stored events');
|
console.log('EOSE received - End of stored events');
|
||||||
console.log('Current config after EOSE:', currentConfig);
|
console.log('Current config after EOSE:', currentConfig);
|
||||||
|
|
||||||
if (!currentConfig) {
|
if (!currentConfig) {
|
||||||
console.log('No configuration events were received');
|
console.log('No configuration events were received');
|
||||||
configStatus.textContent = 'NO CONFIGURATION EVENTS FOUND';
|
configStatus.textContent = 'NO CONFIGURATION EVENTS FOUND';
|
||||||
@@ -720,22 +725,22 @@
|
|||||||
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
||||||
relayStatus.className = 'status connected';
|
relayStatus.className = 'status connected';
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (messageType === "NOTICE") {
|
} else if (messageType === "NOTICE") {
|
||||||
console.log('Received NOTICE:', eventData || message[1]);
|
console.log('Received NOTICE:', eventData || message[1]);
|
||||||
|
|
||||||
} else if (messageType === "OK") {
|
} else if (messageType === "OK") {
|
||||||
console.log('Received OK response:', message);
|
console.log('Received OK response:', message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error('Error parsing message:', parseError);
|
console.error('Error parsing message:', parseError);
|
||||||
console.log('Raw message data:', event.data);
|
console.log('Raw message data:', event.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
configWebSocket.onerror = function(error) {
|
configWebSocket.onerror = function (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.error('WebSocket error:', error);
|
console.error('WebSocket error:', error);
|
||||||
console.error('WebSocket URL that failed:', urlsToTry[0]);
|
console.error('WebSocket URL that failed:', urlsToTry[0]);
|
||||||
@@ -750,7 +755,7 @@
|
|||||||
reject(new Error(`WebSocket connection error to ${urlsToTry[0]}`));
|
reject(new Error(`WebSocket connection error to ${urlsToTry[0]}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
configWebSocket.onclose = function(event) {
|
configWebSocket.onclose = function (event) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.log('WebSocket connection closed:', event.code, event.reason);
|
console.log('WebSocket connection closed:', event.code, event.reason);
|
||||||
relayStatus.textContent = 'CONNECTION CLOSED';
|
relayStatus.textContent = 'CONNECTION CLOSED';
|
||||||
@@ -764,7 +769,7 @@
|
|||||||
console.error('Configuration subscription failed:', error.message);
|
console.error('Configuration subscription failed:', error.message);
|
||||||
console.error('Configuration subscription failed:', error);
|
console.error('Configuration subscription failed:', error);
|
||||||
console.error('Error stack:', error.stack);
|
console.error('Error stack:', error.stack);
|
||||||
|
|
||||||
relayStatus.textContent = 'SUBSCRIPTION FAILED';
|
relayStatus.textContent = 'SUBSCRIPTION FAILED';
|
||||||
relayStatus.className = 'status error';
|
relayStatus.className = 'status error';
|
||||||
return false;
|
return false;
|
||||||
@@ -780,12 +785,12 @@
|
|||||||
try {
|
try {
|
||||||
console.log('=== DISPLAYING CONFIGURATION EVENT ===');
|
console.log('=== DISPLAYING CONFIGURATION EVENT ===');
|
||||||
console.log('Event received for display:', event);
|
console.log('Event received for display:', event);
|
||||||
|
|
||||||
currentConfig = event;
|
currentConfig = event;
|
||||||
|
|
||||||
// Clear existing table
|
// Clear existing table
|
||||||
configTableBody.innerHTML = '';
|
configTableBody.innerHTML = '';
|
||||||
|
|
||||||
// Display basic event info
|
// Display basic event info
|
||||||
const basicInfo = [
|
const basicInfo = [
|
||||||
['Event ID', event.id],
|
['Event ID', event.id],
|
||||||
@@ -794,14 +799,14 @@
|
|||||||
['Kind', event.kind],
|
['Kind', event.kind],
|
||||||
['Content', event.content]
|
['Content', event.content]
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log(`Adding ${basicInfo.length} basic info rows`);
|
console.log(`Adding ${basicInfo.length} basic info rows`);
|
||||||
basicInfo.forEach(([key, value]) => {
|
basicInfo.forEach(([key, value]) => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `<td>${key}</td><td>${value}</td><td>-</td>`;
|
row.innerHTML = `<td>${key}</td><td>${value}</td><td>-</td>`;
|
||||||
configTableBody.appendChild(row);
|
configTableBody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display tags
|
// Display tags
|
||||||
console.log(`Processing ${event.tags.length} tags`);
|
console.log(`Processing ${event.tags.length} tags`);
|
||||||
event.tags.forEach(tag => {
|
event.tags.forEach(tag => {
|
||||||
@@ -811,13 +816,13 @@
|
|||||||
configTableBody.appendChild(row);
|
configTableBody.appendChild(row);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display raw JSON
|
// Display raw JSON
|
||||||
rawConfigJson.textContent = JSON.stringify(event, null, 2);
|
rawConfigJson.textContent = JSON.stringify(event, null, 2);
|
||||||
|
|
||||||
console.log('Configuration display completed successfully');
|
console.log('Configuration display completed successfully');
|
||||||
updateConfigStatus(true);
|
updateConfigStatus(true);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in displayConfiguration:', error.message);
|
console.error('Error in displayConfiguration:', error.message);
|
||||||
console.error('Display configuration error:', error);
|
console.error('Display configuration error:', error);
|
||||||
@@ -832,7 +837,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
configForm.innerHTML = '';
|
configForm.innerHTML = '';
|
||||||
|
|
||||||
// Define field types and validation for different config parameters
|
// Define field types and validation for different config parameters
|
||||||
const fieldTypes = {
|
const fieldTypes = {
|
||||||
'auth_enabled': 'boolean',
|
'auth_enabled': 'boolean',
|
||||||
@@ -897,16 +902,16 @@
|
|||||||
Object.entries(configData).forEach(([key, value]) => {
|
Object.entries(configData).forEach(([key, value]) => {
|
||||||
const fieldType = fieldTypes[key] || 'text';
|
const fieldType = fieldTypes[key] || 'text';
|
||||||
const description = descriptions[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
const description = descriptions[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
|
||||||
const fieldGroup = document.createElement('div');
|
const fieldGroup = document.createElement('div');
|
||||||
fieldGroup.className = 'input-group';
|
fieldGroup.className = 'input-group';
|
||||||
|
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.textContent = description;
|
label.textContent = description;
|
||||||
label.setAttribute('for', `config-${key}`);
|
label.setAttribute('for', `config-${key}`);
|
||||||
|
|
||||||
let input;
|
let input;
|
||||||
|
|
||||||
if (fieldType === 'boolean') {
|
if (fieldType === 'boolean') {
|
||||||
input = document.createElement('select');
|
input = document.createElement('select');
|
||||||
input.innerHTML = `
|
input.innerHTML = `
|
||||||
@@ -923,15 +928,15 @@
|
|||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.value = value;
|
input.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.id = `config-${key}`;
|
input.id = `config-${key}`;
|
||||||
input.name = key;
|
input.name = key;
|
||||||
|
|
||||||
// Make relay_pubkey read-only
|
// Make relay_pubkey read-only
|
||||||
if (key === 'relay_pubkey' || key === 'd') {
|
if (key === 'relay_pubkey' || key === 'd') {
|
||||||
input.disabled = true;
|
input.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldGroup.appendChild(label);
|
fieldGroup.appendChild(label);
|
||||||
fieldGroup.appendChild(input);
|
fieldGroup.appendChild(input);
|
||||||
configForm.appendChild(fieldGroup);
|
configForm.appendChild(fieldGroup);
|
||||||
@@ -945,7 +950,7 @@
|
|||||||
console.log('No configuration loaded to edit');
|
console.log('No configuration loaded to edit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateConfigForm(currentConfig);
|
generateConfigForm(currentConfig);
|
||||||
configViewMode.classList.add('hidden');
|
configViewMode.classList.add('hidden');
|
||||||
configEditMode.classList.remove('hidden');
|
configEditMode.classList.remove('hidden');
|
||||||
@@ -972,18 +977,18 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Building new configuration event...');
|
console.log('Building new configuration event...');
|
||||||
|
|
||||||
// Collect form data
|
// Collect form data
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const formInputs = configForm.querySelectorAll('input, select');
|
const formInputs = configForm.querySelectorAll('input, select');
|
||||||
const newTags = [];
|
const newTags = [];
|
||||||
|
|
||||||
// Preserve the 'd' tag (relay identifier) from original event
|
// Preserve the 'd' tag (relay identifier) from original event
|
||||||
const dTag = currentConfig.tags.find(tag => tag[0] === 'd');
|
const dTag = currentConfig.tags.find(tag => tag[0] === 'd');
|
||||||
if (dTag) {
|
if (dTag) {
|
||||||
newTags.push(dTag);
|
newTags.push(dTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add updated configuration tags
|
// Add updated configuration tags
|
||||||
formInputs.forEach(input => {
|
formInputs.forEach(input => {
|
||||||
if (!input.disabled && input.name) {
|
if (!input.disabled && input.name) {
|
||||||
@@ -1000,11 +1005,11 @@
|
|||||||
content: currentConfig.content || 'C Nostr Relay Configuration'
|
content: currentConfig.content || 'C Nostr Relay Configuration'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Signing event with nostr-lite...');
|
console.log('Signing event with window.nostr...');
|
||||||
|
|
||||||
// Sign the event using nostr-lite
|
// Sign the event using window.nostr (NIP-07 interface)
|
||||||
const signedEvent = await nlLite.signEvent(newEvent);
|
const signedEvent = await window.nostr.signEvent(newEvent);
|
||||||
|
|
||||||
if (!signedEvent || !signedEvent.sig) {
|
if (!signedEvent || !signedEvent.sig) {
|
||||||
throw new Error('Event signing failed - no signature returned');
|
throw new Error('Event signing failed - no signature returned');
|
||||||
}
|
}
|
||||||
@@ -1032,7 +1037,7 @@
|
|||||||
|
|
||||||
// Create a new WebSocket connection for publishing
|
// Create a new WebSocket connection for publishing
|
||||||
const publishWs = new WebSocket(url);
|
const publishWs = new WebSocket(url);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let timeoutId = setTimeout(() => {
|
let timeoutId = setTimeout(() => {
|
||||||
console.error('Publish timeout');
|
console.error('Publish timeout');
|
||||||
@@ -1040,31 +1045,31 @@
|
|||||||
reject(new Error('Publish timeout'));
|
reject(new Error('Publish timeout'));
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
publishWs.onopen = function() {
|
publishWs.onopen = function () {
|
||||||
console.log('Publish WebSocket connected, sending event...');
|
console.log('Publish WebSocket connected, sending event...');
|
||||||
|
|
||||||
// Send EVENT message
|
// Send EVENT message
|
||||||
const eventMessage = ["EVENT", signedEvent];
|
const eventMessage = ["EVENT", signedEvent];
|
||||||
console.log('Sending EVENT message:', JSON.stringify(eventMessage));
|
console.log('Sending EVENT message:', JSON.stringify(eventMessage));
|
||||||
publishWs.send(JSON.stringify(eventMessage));
|
publishWs.send(JSON.stringify(eventMessage));
|
||||||
};
|
};
|
||||||
|
|
||||||
publishWs.onmessage = function(event) {
|
publishWs.onmessage = function (event) {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
console.log('Publish response:', message);
|
console.log('Publish response:', message);
|
||||||
|
|
||||||
if (Array.isArray(message)) {
|
if (Array.isArray(message)) {
|
||||||
const [messageType, eventId, success, errorMsg] = message;
|
const [messageType, eventId, success, errorMsg] = message;
|
||||||
|
|
||||||
if (messageType === "OK") {
|
if (messageType === "OK") {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
publishWs.close();
|
publishWs.close();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log('Configuration published successfully!');
|
console.log('Configuration published successfully!');
|
||||||
console.log('The updated configuration should appear automatically via subscription');
|
console.log('The updated configuration should appear automatically via subscription');
|
||||||
|
|
||||||
// Exit edit mode
|
// Exit edit mode
|
||||||
exitEditMode();
|
exitEditMode();
|
||||||
resolve(true);
|
resolve(true);
|
||||||
@@ -1079,14 +1084,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
publishWs.onerror = function(error) {
|
publishWs.onerror = function (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.error('Publish WebSocket error:', error);
|
console.error('Publish WebSocket error:', error);
|
||||||
publishWs.close();
|
publishWs.close();
|
||||||
reject(new Error('Publish WebSocket error'));
|
reject(new Error('Publish WebSocket error'));
|
||||||
};
|
};
|
||||||
|
|
||||||
publishWs.onclose = function() {
|
publishWs.onclose = function () {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1101,12 +1106,12 @@
|
|||||||
function updateEventPreview() {
|
function updateEventPreview() {
|
||||||
const type = commandType.value;
|
const type = commandType.value;
|
||||||
const payload = commandPayload.value.trim();
|
const payload = commandPayload.value.trim();
|
||||||
|
|
||||||
if (!type || !userPubkey) {
|
if (!type || !userPubkey) {
|
||||||
eventPreview.textContent = 'No event constructed';
|
eventPreview.textContent = 'No event constructed';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = {
|
const event = {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
pubkey: userPubkey,
|
pubkey: userPubkey,
|
||||||
@@ -1119,13 +1124,13 @@
|
|||||||
id: 'EVENT_ID_PLACEHOLDER',
|
id: 'EVENT_ID_PLACEHOLDER',
|
||||||
sig: 'SIGNATURE_PLACEHOLDER'
|
sig: 'SIGNATURE_PLACEHOLDER'
|
||||||
};
|
};
|
||||||
|
|
||||||
eventPreview.textContent = JSON.stringify(event, null, 2);
|
eventPreview.textContent = JSON.stringify(event, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
logoutBtn.addEventListener('click', logout);
|
logoutBtn.addEventListener('click', logout);
|
||||||
fetchConfigBtn.addEventListener('click', function(e) {
|
fetchConfigBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
fetchConfiguration().catch(error => {
|
fetchConfiguration().catch(error => {
|
||||||
@@ -1133,7 +1138,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
copyConfigBtn.addEventListener('click', function(e) {
|
copyConfigBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (currentConfig) {
|
if (currentConfig) {
|
||||||
@@ -1143,13 +1148,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
editConfigBtn.addEventListener('click', function(e) {
|
editConfigBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
enterEditMode();
|
enterEditMode();
|
||||||
});
|
});
|
||||||
|
|
||||||
saveConfigBtn.addEventListener('click', function(e) {
|
saveConfigBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
saveConfiguration().catch(error => {
|
saveConfiguration().catch(error => {
|
||||||
@@ -1157,7 +1162,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cancelEditBtn.addEventListener('click', function(e) {
|
cancelEditBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
exitEditMode();
|
exitEditMode();
|
||||||
@@ -1166,7 +1171,7 @@
|
|||||||
commandType.addEventListener('change', updateEventPreview);
|
commandType.addEventListener('change', updateEventPreview);
|
||||||
commandPayload.addEventListener('input', updateEventPreview);
|
commandPayload.addEventListener('input', updateEventPreview);
|
||||||
|
|
||||||
sendCommandBtn.addEventListener('click', function(e) {
|
sendCommandBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const type = commandType.value;
|
const type = commandType.value;
|
||||||
@@ -1174,11 +1179,11 @@
|
|||||||
console.log('Please select a command type');
|
console.log('Please select a command type');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Command sending not yet implemented: ${type}`);
|
console.log(`Command sending not yet implemented: ${type}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
clearLogBtn.addEventListener('click', function(e) {
|
clearLogBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
logPanel.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Log cleared.</div>';
|
logPanel.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Log cleared.</div>';
|
||||||
@@ -1191,4 +1196,5 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
* Two-file architecture:
|
* Two-file architecture:
|
||||||
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
||||||
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
|
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
|
||||||
* Generated on: 2025-09-15T18:50:50.789Z
|
* Generated on: 2025-09-16T15:52:30.145Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Verify dependencies are loaded
|
// Verify dependencies are loaded
|
||||||
@@ -1128,23 +1128,62 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleExtension() {
|
_handleExtension() {
|
||||||
// Detect all available real extensions
|
// SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension
|
||||||
const availableExtensions = this._detectAllExtensions();
|
let extension = null;
|
||||||
|
|
||||||
console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName));
|
// Check if NostrLite instance has a preserved extension (real extension detected at init)
|
||||||
|
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||||
if (availableExtensions.length === 0) {
|
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||||
console.log('Modal: No real extensions found');
|
console.log('Modal: Using preserved extension:', extension.constructor?.name);
|
||||||
this._showExtensionRequired();
|
|
||||||
} else if (availableExtensions.length === 1) {
|
|
||||||
// Single extension - use it directly without showing choice UI
|
|
||||||
console.log('Modal: Single extension detected, using it directly:', availableExtensions[0].displayName);
|
|
||||||
this._tryExtensionLogin(availableExtensions[0].extension);
|
|
||||||
} else {
|
|
||||||
// Multiple extensions - show choice UI
|
|
||||||
console.log('Modal: Multiple extensions detected, showing choice UI for', availableExtensions.length, 'extensions');
|
|
||||||
this._showExtensionChoice(availableExtensions);
|
|
||||||
}
|
}
|
||||||
|
// Otherwise check current window.nostr
|
||||||
|
else if (window.nostr && this._isRealExtension(window.nostr)) {
|
||||||
|
extension = window.nostr;
|
||||||
|
console.log('Modal: Using current window.nostr extension:', extension.constructor?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extension) {
|
||||||
|
console.log('Modal: No extension detected yet, waiting for deferred detection...');
|
||||||
|
|
||||||
|
// DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 10; // Try for 2 seconds
|
||||||
|
const checkForExtension = () => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
// Check again for preserved extension (might be set by deferred detection)
|
||||||
|
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||||
|
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||||
|
console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name);
|
||||||
|
this._tryExtensionLogin(extension);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current window.nostr again
|
||||||
|
if (window.nostr && this._isRealExtension(window.nostr)) {
|
||||||
|
extension = window.nostr;
|
||||||
|
console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name);
|
||||||
|
this._tryExtensionLogin(extension);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep trying or give up
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
setTimeout(checkForExtension, 200);
|
||||||
|
} else {
|
||||||
|
console.log('Modal: No browser extension found after waiting 2 seconds');
|
||||||
|
this._showExtensionRequired();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start checking after a brief delay
|
||||||
|
setTimeout(checkForExtension, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the single detected extension directly - no choice UI
|
||||||
|
console.log('Modal: Single extension mode - using extension directly');
|
||||||
|
this._tryExtensionLogin(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
_detectAllExtensions() {
|
_detectAllExtensions() {
|
||||||
@@ -1190,17 +1229,38 @@ class Modal {
|
|||||||
|
|
||||||
// Also check window.nostr but be extra careful to avoid our library
|
// Also check window.nostr but be extra careful to avoid our library
|
||||||
console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name);
|
console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name);
|
||||||
if (window.nostr && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
|
|
||||||
extensions.push({
|
if (window.nostr) {
|
||||||
name: 'window.nostr',
|
// Check if window.nostr is our WindowNostr facade with a preserved extension
|
||||||
displayName: 'Extension (window.nostr)',
|
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
|
||||||
icon: '🔑',
|
console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
|
||||||
extension: window.nostr
|
const preservedExtension = window.nostr.existingNostr;
|
||||||
});
|
console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
|
||||||
seenExtensions.add(window.nostr);
|
|
||||||
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
|
||||||
} else if (window.nostr) {
|
extensions.push({
|
||||||
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`);
|
name: 'window.nostr.existingNostr',
|
||||||
|
displayName: 'Extension (preserved by WindowNostr)',
|
||||||
|
icon: '🔑',
|
||||||
|
extension: preservedExtension
|
||||||
|
});
|
||||||
|
seenExtensions.add(preservedExtension);
|
||||||
|
console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if window.nostr is directly a real extension (not our facade)
|
||||||
|
else if (this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
|
||||||
|
extensions.push({
|
||||||
|
name: 'window.nostr',
|
||||||
|
displayName: 'Extension (window.nostr)',
|
||||||
|
icon: '🔑',
|
||||||
|
extension: window.nostr
|
||||||
|
});
|
||||||
|
seenExtensions.add(window.nostr);
|
||||||
|
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
@@ -1790,6 +1850,63 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setAuthMethod(method, options = {}) {
|
_setAuthMethod(method, options = {}) {
|
||||||
|
// SINGLE-EXTENSION ARCHITECTURE: Handle method switching
|
||||||
|
console.log('Modal: _setAuthMethod called with:', method, options);
|
||||||
|
|
||||||
|
// CRITICAL: Never install facade for extension methods - leave window.nostr as the extension
|
||||||
|
if (method === 'extension') {
|
||||||
|
console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension');
|
||||||
|
|
||||||
|
// Emit auth method selection directly for extension
|
||||||
|
const event = new CustomEvent('nlMethodSelected', {
|
||||||
|
detail: { method, ...options }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-extension methods, we need to ensure WindowNostr facade is available
|
||||||
|
console.log('Modal: Non-extension method detected:', method);
|
||||||
|
|
||||||
|
// Check if we have a preserved extension but no WindowNostr facade installed
|
||||||
|
const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension;
|
||||||
|
const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr';
|
||||||
|
|
||||||
|
console.log('Modal: Method switching check:');
|
||||||
|
console.log(' method:', method);
|
||||||
|
console.log(' hasPreservedExtension:', hasPreservedExtension);
|
||||||
|
console.log(' hasWindowNostrFacade:', hasWindowNostrFacade);
|
||||||
|
console.log(' current window.nostr constructor:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
|
// If we have a preserved extension but no facade, install facade for method switching
|
||||||
|
if (hasPreservedExtension && !hasWindowNostrFacade) {
|
||||||
|
console.log('Modal: Installing WindowNostr facade for method switching (non-extension authentication)');
|
||||||
|
|
||||||
|
// Get the NostrLite instance and install facade with preserved extension
|
||||||
|
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
||||||
|
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
||||||
|
const preservedExtension = nostrLiteInstance.preservedExtension;
|
||||||
|
console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name);
|
||||||
|
|
||||||
|
nostrLiteInstance._installFacade(preservedExtension);
|
||||||
|
console.log('Modal: WindowNostr facade installed for method switching');
|
||||||
|
} else {
|
||||||
|
console.error('Modal: Cannot access NostrLite instance or _installFacade method');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no extension at all, ensure facade is installed for local/NIP-46/readonly methods
|
||||||
|
else if (!hasPreservedExtension && !hasWindowNostrFacade) {
|
||||||
|
console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)');
|
||||||
|
|
||||||
|
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
||||||
|
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
||||||
|
nostrLiteInstance._installFacade();
|
||||||
|
console.log('Modal: WindowNostr facade installed for non-extension methods');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Emit auth method selection
|
// Emit auth method selection
|
||||||
const event = new CustomEvent('nlMethodSelected', {
|
const event = new CustomEvent('nlMethodSelected', {
|
||||||
detail: { method, ...options }
|
detail: { method, ...options }
|
||||||
@@ -1823,8 +1940,13 @@ class Modal {
|
|||||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||||
|
|
||||||
const message = document.createElement('p');
|
const message = document.createElement('p');
|
||||||
message.textContent = 'Please install a Nostr browser extension like Alby or getflattr and refresh the page.';
|
message.innerHTML = `
|
||||||
message.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
|
Please install a Nostr browser extension and refresh the page.<br><br>
|
||||||
|
<strong>Important:</strong> If you have multiple extensions installed, please disable all but one to avoid conflicts.
|
||||||
|
<br><br>
|
||||||
|
Popular extensions: Alby, nos2x, Flamingo
|
||||||
|
`;
|
||||||
|
message.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px; line-height: 1.4;';
|
||||||
|
|
||||||
const backButton = document.createElement('button');
|
const backButton = document.createElement('button');
|
||||||
backButton.textContent = 'Back';
|
backButton.textContent = 'Back';
|
||||||
@@ -1867,27 +1989,11 @@ class Modal {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const urlLabel = document.createElement('label');
|
// Users will enter the complete bunker connection string with relay info
|
||||||
urlLabel.textContent = 'Remote URL (optional):';
|
|
||||||
urlLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
|
|
||||||
|
|
||||||
const urlInput = document.createElement('input');
|
|
||||||
urlInput.type = 'url';
|
|
||||||
urlInput.placeholder = 'ws://localhost:8080 (default)';
|
|
||||||
urlInput.style.cssText = `
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Users will enter the bunker URL manually from their bunker setup
|
|
||||||
|
|
||||||
const connectButton = document.createElement('button');
|
const connectButton = document.createElement('button');
|
||||||
connectButton.textContent = 'Connect to Bunker';
|
connectButton.textContent = 'Connect to Bunker';
|
||||||
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, urlInput.value);
|
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value);
|
||||||
connectButton.style.cssText = this._getButtonStyle();
|
connectButton.style.cssText = this._getButtonStyle();
|
||||||
|
|
||||||
const backButton = document.createElement('button');
|
const backButton = document.createElement('button');
|
||||||
@@ -1897,8 +2003,6 @@ class Modal {
|
|||||||
|
|
||||||
formGroup.appendChild(label);
|
formGroup.appendChild(label);
|
||||||
formGroup.appendChild(pubkeyInput);
|
formGroup.appendChild(pubkeyInput);
|
||||||
formGroup.appendChild(urlLabel);
|
|
||||||
formGroup.appendChild(urlInput);
|
|
||||||
|
|
||||||
this.modalBody.appendChild(title);
|
this.modalBody.appendChild(title);
|
||||||
this.modalBody.appendChild(description);
|
this.modalBody.appendChild(description);
|
||||||
@@ -1907,17 +2011,17 @@ class Modal {
|
|||||||
this.modalBody.appendChild(backButton);
|
this.modalBody.appendChild(backButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleNip46Connect(bunkerPubkey, bunkerUrl) {
|
_handleNip46Connect(bunkerPubkey) {
|
||||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||||
this._showError('Bunker pubkey is required');
|
this._showError('Bunker pubkey is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._showNip46Connecting(bunkerPubkey, bunkerUrl);
|
this._showNip46Connecting(bunkerPubkey);
|
||||||
this._performNip46Connect(bunkerPubkey, bunkerUrl);
|
this._performNip46Connect(bunkerPubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
_showNip46Connecting(bunkerPubkey, bunkerUrl) {
|
_showNip46Connecting(bunkerPubkey) {
|
||||||
this.modalBody.innerHTML = '';
|
this.modalBody.innerHTML = '';
|
||||||
|
|
||||||
const title = document.createElement('h3');
|
const title = document.createElement('h3');
|
||||||
@@ -1935,9 +2039,8 @@ class Modal {
|
|||||||
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
||||||
bunkerInfo.innerHTML = `
|
bunkerInfo.innerHTML = `
|
||||||
<strong>Connecting to bunker:</strong><br>
|
<strong>Connecting to bunker:</strong><br>
|
||||||
Pubkey: <code style="word-break: break-all;">${displayPubkey}</code><br>
|
Connection: <code style="word-break: break-all;">${displayPubkey}</code><br>
|
||||||
Relay: <code style="word-break: break-all;">${bunkerUrl || 'ws://localhost:8080'}</code><br>
|
<small style="color: #6b7280;">Connection string contains all necessary relay information.</small>
|
||||||
<small style="color: #6b7280;">If this relay is offline, the bunker server may be unavailable.</small>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const connectingDiv = document.createElement('div');
|
const connectingDiv = document.createElement('div');
|
||||||
@@ -1954,9 +2057,9 @@ class Modal {
|
|||||||
this.modalBody.appendChild(connectingDiv);
|
this.modalBody.appendChild(connectingDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _performNip46Connect(bunkerPubkey, bunkerUrl) {
|
async _performNip46Connect(bunkerPubkey) {
|
||||||
try {
|
try {
|
||||||
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey, bunkerUrl);
|
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
|
||||||
|
|
||||||
// Check if nostr-tools NIP-46 is available
|
// Check if nostr-tools NIP-46 is available
|
||||||
if (!window.NostrTools?.nip46) {
|
if (!window.NostrTools?.nip46) {
|
||||||
@@ -2648,16 +2751,149 @@ class NostrLite {
|
|||||||
|
|
||||||
_setupWindowNostrFacade() {
|
_setupWindowNostrFacade() {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === TRUE SINGLE-EXTENSION ARCHITECTURE ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Initial window.nostr:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Initial window.nostr constructor:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
// Store existing window.nostr if it exists (from extensions)
|
// Store existing window.nostr if it exists (from extensions)
|
||||||
const existingNostr = window.nostr;
|
const existingNostr = window.nostr;
|
||||||
|
|
||||||
// Always install our facade
|
// TRUE SINGLE-EXTENSION ARCHITECTURE: Don't install facade when extensions detected
|
||||||
window.nostr = new WindowNostr(this, existingNostr);
|
if (this._isRealExtension(existingNostr)) {
|
||||||
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed',
|
console.log('NOSTR_LOGIN_LITE: ✓ REAL EXTENSION DETECTED IMMEDIATELY - PRESERVING WITHOUT FACADE');
|
||||||
existingNostr ? '(with extension passthrough)' : '(no existing extension)');
|
console.log('NOSTR_LOGIN_LITE: Extension constructor:', existingNostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(existingNostr));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility');
|
||||||
|
this.preservedExtension = existingNostr;
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
// DON'T install facade - leave window.nostr as the extension
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEFERRED EXTENSION DETECTION: Extensions like nos2x may load after us
|
||||||
|
console.log('NOSTR_LOGIN_LITE: No real extension detected initially, starting deferred detection...');
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
|
||||||
|
let checkCount = 0;
|
||||||
|
const maxChecks = 10; // Check for up to 2 seconds
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
checkCount++;
|
||||||
|
const currentNostr = window.nostr;
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === DEFERRED CHECK ' + checkCount + '/' + maxChecks + ' ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Current window.nostr:', currentNostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Constructor:', currentNostr?.constructor?.name);
|
||||||
|
|
||||||
|
// Skip if it's our facade
|
||||||
|
if (currentNostr?.constructor?.name === 'WindowNostr') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Skipping - this is our facade');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isRealExtension(currentNostr)) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✓✓✓ LATE EXTENSION DETECTED - PRESERVING WITHOUT FACADE ✓✓✓');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension detected after ' + (checkCount * 200) + 'ms!');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension constructor:', currentNostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(currentNostr));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility');
|
||||||
|
this.preservedExtension = currentNostr;
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
// DON'T install facade - leave window.nostr as the extension
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop checking after max attempts - no extension found
|
||||||
|
if (checkCount >= maxChecks) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ⚠️ MAX CHECKS REACHED - NO EXTENSION FOUND');
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Installing facade for local/NIP-46/readonly methods');
|
||||||
|
this._installFacade();
|
||||||
|
}
|
||||||
|
}, 200); // Check every 200ms
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Waiting for deferred detection to complete...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_installFacade(existingNostr = null) {
|
||||||
|
if (typeof window !== 'undefined' && !this.facadeInstalled) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === _installFacade CALLED ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
|
const facade = new WindowNostr(this, existingNostr);
|
||||||
|
window.nostr = facade;
|
||||||
|
this.facadeInstalled = true;
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === FACADE INSTALLED WITH EXTENSION ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to identify real browser extensions
|
||||||
|
_isRealExtension(obj) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === _isRealExtension DEBUG ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: obj:', obj);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj);
|
||||||
|
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Not an object');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Object keys:', Object.keys(obj));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: getPublicKey type:', typeof obj.getPublicKey);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: signEvent type:', typeof obj.signEvent);
|
||||||
|
|
||||||
|
// Must have required Nostr methods
|
||||||
|
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Missing required methods');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude our own library classes
|
||||||
|
const constructorName = obj.constructor?.name;
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName);
|
||||||
|
|
||||||
|
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Is our library class');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude NostrTools library object
|
||||||
|
if (obj === window.NostrTools) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real extensions typically have internal properties or specific characteristics
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension property check:');
|
||||||
|
console.log(' _isEnabled:', !!obj._isEnabled);
|
||||||
|
console.log(' enabled:', !!obj.enabled);
|
||||||
|
console.log(' kind:', !!obj.kind);
|
||||||
|
console.log(' _eventEmitter:', !!obj._eventEmitter);
|
||||||
|
console.log(' _scope:', !!obj._scope);
|
||||||
|
console.log(' _requests:', !!obj._requests);
|
||||||
|
console.log(' _pubkey:', !!obj._pubkey);
|
||||||
|
console.log(' name:', !!obj.name);
|
||||||
|
console.log(' version:', !!obj.version);
|
||||||
|
console.log(' description:', !!obj.description);
|
||||||
|
|
||||||
|
const hasExtensionProps = !!(
|
||||||
|
obj._isEnabled || obj.enabled || obj.kind ||
|
||||||
|
obj._eventEmitter || obj._scope || obj._requests || obj._pubkey ||
|
||||||
|
obj.name || obj.version || obj.description
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension detection result for', constructorName, ':', hasExtensionProps);
|
||||||
|
return hasExtensionProps;
|
||||||
|
}
|
||||||
|
|
||||||
launch(startScreen = 'login') {
|
launch(startScreen = 'login') {
|
||||||
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
||||||
|
|
||||||
|
|||||||
797
src/config.c
797
src/config.c
@@ -18,12 +18,57 @@ extern sqlite3* g_db;
|
|||||||
config_manager_t g_config_manager = {0};
|
config_manager_t g_config_manager = {0};
|
||||||
char g_database_path[512] = {0};
|
char g_database_path[512] = {0};
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// NEW ADMIN API STRUCTURES
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Migration state management
|
||||||
|
typedef enum {
|
||||||
|
MIGRATION_NOT_NEEDED,
|
||||||
|
MIGRATION_NEEDED,
|
||||||
|
MIGRATION_IN_PROGRESS,
|
||||||
|
MIGRATION_COMPLETED,
|
||||||
|
MIGRATION_FAILED
|
||||||
|
} migration_state_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
migration_state_t state;
|
||||||
|
int event_config_count;
|
||||||
|
int table_config_count;
|
||||||
|
int migration_errors;
|
||||||
|
time_t migration_started;
|
||||||
|
time_t migration_completed;
|
||||||
|
char error_message[512];
|
||||||
|
} migration_status_t;
|
||||||
|
|
||||||
|
static migration_status_t g_migration_status = {0};
|
||||||
|
|
||||||
|
// Configuration source type
|
||||||
|
typedef enum {
|
||||||
|
CONFIG_SOURCE_EVENT, // Current event-based system
|
||||||
|
CONFIG_SOURCE_TABLE, // New table-based system
|
||||||
|
CONFIG_SOURCE_HYBRID // During migration
|
||||||
|
} config_source_t;
|
||||||
|
|
||||||
// Logging functions (defined in main.c)
|
// Logging functions (defined in main.c)
|
||||||
extern void log_info(const char* message);
|
extern void log_info(const char* message);
|
||||||
extern void log_success(const char* message);
|
extern void log_success(const char* message);
|
||||||
extern void log_warning(const char* message);
|
extern void log_warning(const char* message);
|
||||||
extern void log_error(const char* message);
|
extern void log_error(const char* message);
|
||||||
|
|
||||||
|
// Forward declarations for new admin API functions
|
||||||
|
int populate_default_config_values(void);
|
||||||
|
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);
|
||||||
|
void invalidate_config_cache(void);
|
||||||
|
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||||
|
const char* pattern_value, const char* action);
|
||||||
|
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||||
|
const char* pattern_value);
|
||||||
|
int is_config_table_ready(void);
|
||||||
|
int migrate_config_from_events_to_table(void);
|
||||||
|
int populate_config_table_from_event(const cJSON* event);
|
||||||
|
|
||||||
// Current configuration cache
|
// Current configuration cache
|
||||||
static cJSON* g_current_config = NULL;
|
static cJSON* g_current_config = NULL;
|
||||||
|
|
||||||
@@ -811,12 +856,12 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Try to store configuration event in database, but cache it if database isn't ready
|
// 7. Process configuration through admin API instead of storing in events table
|
||||||
if (store_config_event_in_database(config_event) == 0) {
|
if (process_startup_config_event_with_fallback(config_event) == 0) {
|
||||||
log_success("Initial configuration event stored successfully");
|
log_success("Initial configuration processed successfully through admin API");
|
||||||
} else {
|
} else {
|
||||||
log_warning("Failed to store initial configuration event - will retry after database init");
|
log_warning("Failed to process initial configuration - will retry after database init");
|
||||||
// Cache the event for later storage
|
// Cache the event for later processing
|
||||||
if (g_pending_config_event) {
|
if (g_pending_config_event) {
|
||||||
cJSON_Delete(g_pending_config_event);
|
cJSON_Delete(g_pending_config_event);
|
||||||
}
|
}
|
||||||
@@ -873,6 +918,9 @@ int startup_existing_relay(const char* relay_pubkey) {
|
|||||||
g_database_path[sizeof(g_database_path) - 1] = '\0';
|
g_database_path[sizeof(g_database_path) - 1] = '\0';
|
||||||
free(db_name);
|
free(db_name);
|
||||||
|
|
||||||
|
// Configuration will be migrated from events to table after database initialization
|
||||||
|
log_info("Configuration migration will be performed after database is available");
|
||||||
|
|
||||||
// Load configuration event from database (after database is initialized)
|
// Load configuration event from database (after database is initialized)
|
||||||
// This will be done in apply_configuration_from_database()
|
// This will be done in apply_configuration_from_database()
|
||||||
|
|
||||||
@@ -1579,6 +1627,490 @@ int handle_configuration_event(cJSON* event, char* error_message, size_t error_s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// NEW ADMIN API IMPLEMENTATION
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// CONFIG TABLE MANAGEMENT FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Note: Config table is now created via embedded schema in sql_schema.h
|
||||||
|
|
||||||
|
// Get value from config table
|
||||||
|
const char* get_config_value_from_table(const char* key) {
|
||||||
|
if (!g_db || !key) return NULL;
|
||||||
|
|
||||||
|
const char* sql = "SELECT value FROM config WHERE key = ?";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
static char config_value_buffer[CONFIG_VALUE_MAX_LENGTH];
|
||||||
|
const char* result = NULL;
|
||||||
|
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
const char* value = (char*)sqlite3_column_text(stmt, 0);
|
||||||
|
if (value) {
|
||||||
|
strncpy(config_value_buffer, value, sizeof(config_value_buffer) - 1);
|
||||||
|
config_value_buffer[sizeof(config_value_buffer) - 1] = '\0';
|
||||||
|
result = config_value_buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set value in config table
|
||||||
|
int set_config_value_in_table(const char* key, const char* value, const char* data_type,
|
||||||
|
const char* description, const char* category, int requires_restart) {
|
||||||
|
if (!g_db || !key || !value || !data_type) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_int(stmt, 6, requires_restart);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config in table (simpler version of set_config_value_in_table)
|
||||||
|
int update_config_in_table(const char* key, const char* value) {
|
||||||
|
if (!g_db || !key || !value) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, key, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate default config values
|
||||||
|
int populate_default_config_values(void) {
|
||||||
|
log_info("Populating default configuration values in table...");
|
||||||
|
|
||||||
|
// Add all default configuration values to the table
|
||||||
|
for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) {
|
||||||
|
const char* key = DEFAULT_CONFIG_VALUES[i].key;
|
||||||
|
const char* value = DEFAULT_CONFIG_VALUES[i].value;
|
||||||
|
|
||||||
|
// Determine data type
|
||||||
|
const char* data_type = "string";
|
||||||
|
if (strcmp(key, "relay_port") == 0 ||
|
||||||
|
strcmp(key, "max_connections") == 0 ||
|
||||||
|
strcmp(key, "pow_min_difficulty") == 0 ||
|
||||||
|
strcmp(key, "max_subscriptions_per_client") == 0 ||
|
||||||
|
strcmp(key, "max_total_subscriptions") == 0 ||
|
||||||
|
strcmp(key, "max_filters_per_subscription") == 0 ||
|
||||||
|
strcmp(key, "max_event_tags") == 0 ||
|
||||||
|
strcmp(key, "max_content_length") == 0 ||
|
||||||
|
strcmp(key, "max_message_length") == 0 ||
|
||||||
|
strcmp(key, "default_limit") == 0 ||
|
||||||
|
strcmp(key, "max_limit") == 0 ||
|
||||||
|
strcmp(key, "nip42_challenge_expiration") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_grace_period") == 0) {
|
||||||
|
data_type = "integer";
|
||||||
|
} else if (strcmp(key, "auth_enabled") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_enabled") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_strict") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_filter") == 0 ||
|
||||||
|
strcmp(key, "nip42_auth_required") == 0) {
|
||||||
|
data_type = "boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set category
|
||||||
|
const char* category = "general";
|
||||||
|
if (strstr(key, "relay_")) {
|
||||||
|
category = "relay";
|
||||||
|
} else if (strstr(key, "nip40_")) {
|
||||||
|
category = "expiration";
|
||||||
|
} else if (strstr(key, "nip42_") || strstr(key, "auth_")) {
|
||||||
|
category = "authentication";
|
||||||
|
} else if (strstr(key, "pow_")) {
|
||||||
|
category = "proof_of_work";
|
||||||
|
} else if (strstr(key, "max_")) {
|
||||||
|
category = "limits";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if requires restart
|
||||||
|
int requires_restart = 0;
|
||||||
|
if (strcmp(key, "relay_port") == 0) {
|
||||||
|
requires_restart = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) != 0) {
|
||||||
|
char error_msg[256];
|
||||||
|
snprintf(error_msg, sizeof(error_msg), "Failed to set default config: %s = %s", key, value);
|
||||||
|
log_error(error_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success("Default configuration values populated");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// ADMIN EVENT PROCESSING FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Process admin events (moved from main.c)
|
||||||
|
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) {
|
||||||
|
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
|
if (!kind_obj || !cJSON_IsNumber(kind_obj)) {
|
||||||
|
snprintf(error_message, error_size, "invalid: missing or invalid kind");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify admin authorization
|
||||||
|
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
|
||||||
|
snprintf(error_message, error_size, "invalid: missing pubkey");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* event_pubkey = cJSON_GetStringValue(pubkey_obj);
|
||||||
|
const char* admin_pubkey = get_config_value("admin_pubkey");
|
||||||
|
|
||||||
|
if (!admin_pubkey || strcmp(event_pubkey, admin_pubkey) != 0) {
|
||||||
|
snprintf(error_message, error_size, "auth-required: not authorized admin");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case 33334:
|
||||||
|
return process_admin_config_event(event, error_message, error_size);
|
||||||
|
case 33335:
|
||||||
|
return process_admin_auth_event(event, error_message, error_size);
|
||||||
|
default:
|
||||||
|
snprintf(error_message, error_size, "invalid: unsupported admin event kind");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle kind 33334 config events
|
||||||
|
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) {
|
||||||
|
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||||
|
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
|
||||||
|
snprintf(error_message, error_size, "invalid: configuration event must have tags");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config table should already exist from embedded schema
|
||||||
|
|
||||||
|
// Begin transaction for atomic config updates
|
||||||
|
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
snprintf(error_message, error_size, "failed to begin config transaction");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int updates_applied = 0;
|
||||||
|
|
||||||
|
// Process each tag as a configuration parameter
|
||||||
|
cJSON* tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags_obj) {
|
||||||
|
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||||
|
|
||||||
|
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* key = cJSON_GetStringValue(tag_name);
|
||||||
|
const char* value = cJSON_GetStringValue(tag_value);
|
||||||
|
|
||||||
|
// Skip relay identifier tag
|
||||||
|
if (strcmp(key, "d") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update configuration in table
|
||||||
|
if (update_config_in_table(key, value) == 0) {
|
||||||
|
updates_applied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates_applied > 0) {
|
||||||
|
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
|
||||||
|
invalidate_config_cache();
|
||||||
|
|
||||||
|
char success_msg[256];
|
||||||
|
snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates", updates_applied);
|
||||||
|
log_success(success_msg);
|
||||||
|
} else {
|
||||||
|
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
|
||||||
|
snprintf(error_message, error_size, "no valid configuration parameters found");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle kind 33335 auth rule events
|
||||||
|
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) {
|
||||||
|
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||||
|
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
|
||||||
|
snprintf(error_message, error_size, "invalid: auth rule event must have tags");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract action from content or tags
|
||||||
|
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
|
||||||
|
const char* content = content_obj ? cJSON_GetStringValue(content_obj) : "";
|
||||||
|
|
||||||
|
// Parse the action from content (should be "add" or "remove")
|
||||||
|
cJSON* content_json = cJSON_Parse(content);
|
||||||
|
const char* action = "add"; // default
|
||||||
|
if (content_json) {
|
||||||
|
cJSON* action_obj = cJSON_GetObjectItem(content_json, "action");
|
||||||
|
if (action_obj && cJSON_IsString(action_obj)) {
|
||||||
|
action = cJSON_GetStringValue(action_obj);
|
||||||
|
}
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction for atomic auth rule updates
|
||||||
|
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
snprintf(error_message, error_size, "failed to begin auth rule transaction");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rules_processed = 0;
|
||||||
|
|
||||||
|
// Process each tag as an auth rule specification
|
||||||
|
cJSON* tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags_obj) {
|
||||||
|
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* rule_type_obj = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* pattern_type_obj = cJSON_GetArrayItem(tag, 1);
|
||||||
|
cJSON* pattern_value_obj = cJSON_GetArrayItem(tag, 2);
|
||||||
|
|
||||||
|
if (!cJSON_IsString(rule_type_obj) ||
|
||||||
|
!cJSON_IsString(pattern_type_obj) ||
|
||||||
|
!cJSON_IsString(pattern_value_obj)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* rule_type = cJSON_GetStringValue(rule_type_obj);
|
||||||
|
const char* pattern_type = cJSON_GetStringValue(pattern_type_obj);
|
||||||
|
const char* pattern_value = cJSON_GetStringValue(pattern_value_obj);
|
||||||
|
|
||||||
|
// Process the auth rule based on action
|
||||||
|
if (strcmp(action, "add") == 0) {
|
||||||
|
if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) {
|
||||||
|
rules_processed++;
|
||||||
|
}
|
||||||
|
} else if (strcmp(action, "remove") == 0) {
|
||||||
|
if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) {
|
||||||
|
rules_processed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules_processed > 0) {
|
||||||
|
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
|
||||||
|
|
||||||
|
char success_msg[256];
|
||||||
|
snprintf(success_msg, sizeof(success_msg), "Processed %d auth rule updates", rules_processed);
|
||||||
|
log_success(success_msg);
|
||||||
|
} else {
|
||||||
|
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
|
||||||
|
snprintf(error_message, error_size, "no valid auth rules found");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// AUTH RULES MANAGEMENT FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Add auth rule from configuration
|
||||||
|
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||||
|
const char* pattern_value, const char* action) {
|
||||||
|
if (!g_db || !rule_type || !pattern_type || !pattern_value || !action) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action) "
|
||||||
|
"VALUES (?, ?, ?, ?)";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 4, action, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove auth rule from configuration
|
||||||
|
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||||
|
const char* pattern_value) {
|
||||||
|
if (!g_db || !rule_type || !pattern_type || !pattern_value) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// CONFIGURATION CACHE MANAGEMENT
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Invalidate configuration cache
|
||||||
|
void invalidate_config_cache(void) {
|
||||||
|
// For now, just log that cache was invalidated
|
||||||
|
// In a full implementation, this would clear any cached config values
|
||||||
|
log_info("Configuration cache invalidated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload configuration from table
|
||||||
|
int reload_config_from_table(void) {
|
||||||
|
// For now, just log that config was reloaded
|
||||||
|
// In a full implementation, this would reload all cached values from the table
|
||||||
|
log_info("Configuration reloaded from table");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// HYBRID CONFIG ACCESS FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Hybrid config getter (tries table first, falls back to event)
|
||||||
|
const char* get_config_value_hybrid(const char* key) {
|
||||||
|
// Try table-based config first if available
|
||||||
|
if (is_config_table_ready()) {
|
||||||
|
const char* table_value = get_config_value_from_table(key);
|
||||||
|
if (table_value) {
|
||||||
|
return table_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to event-based config
|
||||||
|
return get_config_value(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if config table is ready
|
||||||
|
int is_config_table_ready(void) {
|
||||||
|
if (!g_db) return 0;
|
||||||
|
|
||||||
|
const char* sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='config'";
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int table_exists = 0;
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
table_exists = sqlite3_column_int(stmt, 0) > 0;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (!table_exists) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if table has configuration data
|
||||||
|
const char* count_sql = "SELECT COUNT(*) FROM config";
|
||||||
|
rc = sqlite3_prepare_v2(g_db, count_sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int config_count = 0;
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
config_count = sqlite3_column_int(stmt, 0);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
return config_count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize configuration system with migration support
|
||||||
|
int initialize_config_system_with_migration(void) {
|
||||||
|
log_info("Initializing configuration system with migration support...");
|
||||||
|
|
||||||
|
// Initialize config manager
|
||||||
|
memset(&g_config_manager, 0, sizeof(g_config_manager));
|
||||||
|
memset(&g_migration_status, 0, sizeof(g_migration_status));
|
||||||
|
|
||||||
|
// For new installations, config table should already exist from embedded schema
|
||||||
|
log_success("Configuration system initialized with table support");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// RETRY INITIAL CONFIG EVENT STORAGE
|
// RETRY INITIAL CONFIG EVENT STORAGE
|
||||||
// ================================
|
// ================================
|
||||||
@@ -1591,16 +2123,263 @@ int retry_store_initial_config_event(void) {
|
|||||||
|
|
||||||
log_info("Retrying storage of initial configuration event...");
|
log_info("Retrying storage of initial configuration event...");
|
||||||
|
|
||||||
// Try to store the cached configuration event
|
// Try to process the cached configuration event through admin API
|
||||||
if (store_config_event_in_database(g_pending_config_event) == 0) {
|
if (process_startup_config_event_with_fallback(g_pending_config_event) == 0) {
|
||||||
log_success("Initial configuration event stored successfully on retry");
|
log_success("Initial configuration processed successfully through admin API on retry");
|
||||||
|
|
||||||
// Clean up the pending event
|
// Clean up the pending event
|
||||||
cJSON_Delete(g_pending_config_event);
|
cJSON_Delete(g_pending_config_event);
|
||||||
g_pending_config_event = NULL;
|
g_pending_config_event = NULL;
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
log_error("Failed to store initial configuration event on retry");
|
log_error("Failed to process initial configuration through admin API on retry");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// CONFIG MIGRATION FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Populate config table from a configuration event
|
||||||
|
int populate_config_table_from_event(const cJSON* event) {
|
||||||
|
if (!event || !g_db) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("Populating config table from configuration event...");
|
||||||
|
|
||||||
|
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
if (!tags || !cJSON_IsArray(tags)) {
|
||||||
|
log_error("Configuration event missing tags array");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int configs_populated = 0;
|
||||||
|
|
||||||
|
// Process each tag as a configuration parameter
|
||||||
|
cJSON* tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags) {
|
||||||
|
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||||
|
|
||||||
|
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* key = cJSON_GetStringValue(tag_name);
|
||||||
|
const char* value = cJSON_GetStringValue(tag_value);
|
||||||
|
|
||||||
|
// Skip relay identifier tag
|
||||||
|
if (strcmp(key, "d") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine data type for the config value
|
||||||
|
const char* data_type = "string";
|
||||||
|
if (strcmp(key, "relay_port") == 0 ||
|
||||||
|
strcmp(key, "max_connections") == 0 ||
|
||||||
|
strcmp(key, "pow_min_difficulty") == 0 ||
|
||||||
|
strcmp(key, "max_subscriptions_per_client") == 0 ||
|
||||||
|
strcmp(key, "max_total_subscriptions") == 0 ||
|
||||||
|
strcmp(key, "max_filters_per_subscription") == 0 ||
|
||||||
|
strcmp(key, "max_event_tags") == 0 ||
|
||||||
|
strcmp(key, "max_content_length") == 0 ||
|
||||||
|
strcmp(key, "max_message_length") == 0 ||
|
||||||
|
strcmp(key, "default_limit") == 0 ||
|
||||||
|
strcmp(key, "max_limit") == 0 ||
|
||||||
|
strcmp(key, "nip42_challenge_expiration") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_grace_period") == 0) {
|
||||||
|
data_type = "integer";
|
||||||
|
} else if (strcmp(key, "auth_enabled") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_enabled") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_strict") == 0 ||
|
||||||
|
strcmp(key, "nip40_expiration_filter") == 0 ||
|
||||||
|
strcmp(key, "nip42_auth_required") == 0) {
|
||||||
|
data_type = "boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set category
|
||||||
|
const char* category = "general";
|
||||||
|
if (strstr(key, "relay_")) {
|
||||||
|
category = "relay";
|
||||||
|
} else if (strstr(key, "nip40_")) {
|
||||||
|
category = "expiration";
|
||||||
|
} else if (strstr(key, "nip42_") || strstr(key, "auth_")) {
|
||||||
|
category = "authentication";
|
||||||
|
} else if (strstr(key, "pow_")) {
|
||||||
|
category = "proof_of_work";
|
||||||
|
} else if (strstr(key, "max_")) {
|
||||||
|
category = "limits";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if requires restart
|
||||||
|
int requires_restart = 0;
|
||||||
|
if (strcmp(key, "relay_port") == 0) {
|
||||||
|
requires_restart = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert into config table
|
||||||
|
if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) == 0) {
|
||||||
|
configs_populated++;
|
||||||
|
} else {
|
||||||
|
char error_msg[256];
|
||||||
|
snprintf(error_msg, sizeof(error_msg), "Failed to populate config: %s = %s", key, value);
|
||||||
|
log_error(error_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configs_populated > 0) {
|
||||||
|
char success_msg[256];
|
||||||
|
snprintf(success_msg, sizeof(success_msg), "Populated %d configuration values from event", configs_populated);
|
||||||
|
log_success(success_msg);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
log_error("No configuration values were populated from event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate configuration from existing events to config table
|
||||||
|
int migrate_config_from_events_to_table(void) {
|
||||||
|
if (!g_db) {
|
||||||
|
log_error("Database not available for configuration migration");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("Migrating configuration from events to config table...");
|
||||||
|
|
||||||
|
// Load the most recent configuration event from database
|
||||||
|
cJSON* config_event = load_config_event_from_database(g_config_manager.relay_pubkey);
|
||||||
|
if (!config_event) {
|
||||||
|
log_info("No existing configuration event found - migration not needed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate config table from the event
|
||||||
|
int result = populate_config_table_from_event(config_event);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
cJSON_Delete(config_event);
|
||||||
|
|
||||||
|
if (result == 0) {
|
||||||
|
log_success("Configuration migration from events to table completed successfully");
|
||||||
|
} else {
|
||||||
|
log_error("Configuration migration from events to table failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// STARTUP CONFIGURATION PROCESSING
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Process startup configuration event - bypasses auth and updates config table
|
||||||
|
int process_startup_config_event(const cJSON* event) {
|
||||||
|
if (!event || !g_db) {
|
||||||
|
log_error("Invalid parameters for startup config processing");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("Processing startup configuration event through admin API...");
|
||||||
|
|
||||||
|
// Validate event structure first
|
||||||
|
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
|
if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) {
|
||||||
|
log_error("Invalid event kind for startup configuration");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||||
|
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
|
||||||
|
log_error("Startup configuration event missing tags");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction for atomic config updates
|
||||||
|
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
log_error("Failed to begin startup config transaction");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int updates_applied = 0;
|
||||||
|
|
||||||
|
// Process each tag as a configuration parameter (same logic as process_admin_config_event)
|
||||||
|
cJSON* tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags_obj) {
|
||||||
|
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||||
|
|
||||||
|
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* key = cJSON_GetStringValue(tag_name);
|
||||||
|
const char* value = cJSON_GetStringValue(tag_value);
|
||||||
|
|
||||||
|
// Skip relay identifier tag and relay_pubkey (already in table)
|
||||||
|
if (strcmp(key, "d") == 0 || strcmp(key, "relay_pubkey") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update configuration in table
|
||||||
|
if (update_config_in_table(key, value) == 0) {
|
||||||
|
updates_applied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates_applied > 0) {
|
||||||
|
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
|
||||||
|
invalidate_config_cache();
|
||||||
|
|
||||||
|
char success_msg[256];
|
||||||
|
snprintf(success_msg, sizeof(success_msg),
|
||||||
|
"Processed startup configuration: %d values updated in config table", updates_applied);
|
||||||
|
log_success(success_msg);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
|
||||||
|
log_error("No valid configuration parameters found in startup event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process startup configuration event with fallback - for retry scenarios
|
||||||
|
int process_startup_config_event_with_fallback(const cJSON* event) {
|
||||||
|
if (!event) {
|
||||||
|
log_error("Invalid event for startup config processing with fallback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to process through admin API first
|
||||||
|
if (process_startup_config_event(event) == 0) {
|
||||||
|
log_success("Startup configuration processed successfully through admin API");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If that fails, populate defaults and try again
|
||||||
|
log_warning("Startup config processing failed - ensuring defaults are populated");
|
||||||
|
if (populate_default_config_values() != 0) {
|
||||||
|
log_error("Failed to populate default config values");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry processing
|
||||||
|
if (process_startup_config_event(event) == 0) {
|
||||||
|
log_success("Startup configuration processed successfully after populating defaults");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error("Startup configuration processing failed even after populating defaults");
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
39
src/config.h
39
src/config.h
@@ -90,4 +90,43 @@ int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_k
|
|||||||
int is_nip42_auth_required_for_kind(int event_kind);
|
int is_nip42_auth_required_for_kind(int event_kind);
|
||||||
int is_nip42_auth_globally_required(void);
|
int is_nip42_auth_globally_required(void);
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// NEW ADMIN API FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Config table management functions (config table created via embedded schema)
|
||||||
|
const char* get_config_value_from_table(const char* key);
|
||||||
|
int set_config_value_in_table(const char* key, const char* value, const char* data_type,
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Admin event processing functions
|
||||||
|
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Auth rules management functions
|
||||||
|
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||||
|
const char* pattern_value, const char* action);
|
||||||
|
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||||
|
const char* pattern_value);
|
||||||
|
|
||||||
|
// Configuration cache management
|
||||||
|
void invalidate_config_cache(void);
|
||||||
|
int reload_config_from_table(void);
|
||||||
|
|
||||||
|
// Hybrid config access functions
|
||||||
|
const char* get_config_value_hybrid(const char* key);
|
||||||
|
int is_config_table_ready(void);
|
||||||
|
|
||||||
|
// Migration support functions
|
||||||
|
int initialize_config_system_with_migration(void);
|
||||||
|
int migrate_config_from_events_to_table(void);
|
||||||
|
int populate_config_table_from_event(const cJSON* event);
|
||||||
|
|
||||||
|
// Startup configuration processing functions
|
||||||
|
int process_startup_config_event(const cJSON* event);
|
||||||
|
int process_startup_config_event_with_fallback(const cJSON* event);
|
||||||
|
|
||||||
#endif /* CONFIG_H */
|
#endif /* CONFIG_H */
|
||||||
51
src/main.c
51
src/main.c
@@ -227,6 +227,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length);
|
|||||||
// Forward declaration for configuration event handling (kind 33334)
|
// Forward declaration for configuration event handling (kind 33334)
|
||||||
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
|
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
|
||||||
|
// Forward declaration for admin event processing (kinds 33334 and 33335)
|
||||||
|
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
|
||||||
// Forward declaration for NOTICE message support
|
// Forward declaration for NOTICE message support
|
||||||
void send_notice_message(struct lws* wsi, const char* message);
|
void send_notice_message(struct lws* wsi, const char* message);
|
||||||
|
|
||||||
@@ -3092,17 +3095,47 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
// Cleanup event JSON string
|
// Cleanup event JSON string
|
||||||
free(event_json_str);
|
free(event_json_str);
|
||||||
|
|
||||||
// Store event in database and broadcast to subscriptions
|
// Check for admin events (kinds 33334 and 33335) and intercept them
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
// Store the event in the database first
|
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
if (store_event(event) != 0) {
|
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||||
log_error("Failed to store event in database");
|
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||||
result = -1;
|
|
||||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
if (event_kind == 33334 || event_kind == 33335) {
|
||||||
|
// This is an admin event - process it through the admin API instead of normal storage
|
||||||
|
log_info("Admin event detected, processing through admin API");
|
||||||
|
|
||||||
|
char admin_error[512] = {0};
|
||||||
|
if (process_admin_event_in_config(event, admin_error, sizeof(admin_error)) != 0) {
|
||||||
|
log_error("Failed to process admin event through admin API");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, admin_error, sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
log_success("Admin event processed successfully through admin API");
|
||||||
|
// Admin events are processed by the admin API, not broadcast to subscriptions
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regular event - store in database and broadcast
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
log_error("Failed to store event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
log_info("Event stored successfully in database");
|
||||||
|
// Broadcast event to matching persistent subscriptions
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log_info("Event stored successfully in database");
|
// Event without valid kind - try normal storage
|
||||||
// Broadcast event to matching persistent subscriptions
|
if (store_event(event) != 0) {
|
||||||
broadcast_event_to_subscriptions(event);
|
log_error("Failed to store event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
log_info("Event stored successfully in database");
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* Embedded SQL Schema for C Nostr Relay
|
/* Embedded SQL Schema for C Nostr Relay
|
||||||
* Generated from db/schema.sql - Do not edit manually
|
* Generated from db/schema.sql - Do not edit manually
|
||||||
* Schema Version: 6
|
* Schema Version: 7
|
||||||
*/
|
*/
|
||||||
#ifndef SQL_SCHEMA_H
|
#ifndef SQL_SCHEMA_H
|
||||||
#define SQL_SCHEMA_H
|
#define SQL_SCHEMA_H
|
||||||
|
|
||||||
/* Schema version constant */
|
/* Schema version constant */
|
||||||
#define EMBEDDED_SCHEMA_VERSION "6"
|
#define EMBEDDED_SCHEMA_VERSION "7"
|
||||||
|
|
||||||
/* Embedded SQL schema as C string literal */
|
/* Embedded SQL schema as C string literal */
|
||||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
static const char* const EMBEDDED_SCHEMA_SQL =
|
||||||
@@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
|
|||||||
-- Event-based configuration system using kind 33334 Nostr events\n\
|
-- Event-based configuration system using kind 33334 Nostr events\n\
|
||||||
\n\
|
\n\
|
||||||
-- Schema version tracking\n\
|
-- Schema version tracking\n\
|
||||||
PRAGMA user_version = 6;\n\
|
PRAGMA user_version = 7;\n\
|
||||||
\n\
|
\n\
|
||||||
-- Enable foreign key support\n\
|
-- Enable foreign key support\n\
|
||||||
PRAGMA foreign_keys = ON;\n\
|
PRAGMA foreign_keys = ON;\n\
|
||||||
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
|
|||||||
\n\
|
\n\
|
||||||
-- Insert schema metadata\n\
|
-- Insert schema metadata\n\
|
||||||
INSERT INTO schema_info (key, value) VALUES\n\
|
INSERT INTO schema_info (key, value) VALUES\n\
|
||||||
('version', '6'),\n\
|
('version', '7'),\n\
|
||||||
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
|
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\
|
||||||
('created_at', strftime('%s', 'now'));\n\
|
('created_at', strftime('%s', 'now'));\n\
|
||||||
\n\
|
\n\
|
||||||
-- Helper views for common queries\n\
|
-- Helper views for common queries\n\
|
||||||
@@ -154,6 +154,60 @@ CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\
|
|||||||
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);\n\
|
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);\n\
|
||||||
CREATE INDEX idx_auth_rules_active ON auth_rules(active);\n\
|
CREATE INDEX idx_auth_rules_active ON auth_rules(active);\n\
|
||||||
\n\
|
\n\
|
||||||
|
-- Configuration Table for Table-Based Config Management\n\
|
||||||
|
-- Hybrid system supporting both event-based and table-based configuration\n\
|
||||||
|
CREATE TABLE config (\n\
|
||||||
|
key TEXT PRIMARY KEY,\n\
|
||||||
|
value TEXT NOT NULL,\n\
|
||||||
|
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n\
|
||||||
|
description TEXT,\n\
|
||||||
|
category TEXT DEFAULT 'general',\n\
|
||||||
|
requires_restart INTEGER DEFAULT 0,\n\
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||||
|
);\n\
|
||||||
|
\n\
|
||||||
|
-- Indexes for config table performance\n\
|
||||||
|
CREATE INDEX idx_config_category ON config(category);\n\
|
||||||
|
CREATE INDEX idx_config_restart ON config(requires_restart);\n\
|
||||||
|
CREATE INDEX idx_config_updated ON config(updated_at DESC);\n\
|
||||||
|
\n\
|
||||||
|
-- Trigger to update config timestamp on changes\n\
|
||||||
|
CREATE TRIGGER update_config_timestamp\n\
|
||||||
|
AFTER UPDATE ON config\n\
|
||||||
|
FOR EACH ROW\n\
|
||||||
|
BEGIN\n\
|
||||||
|
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\
|
||||||
|
END;\n\
|
||||||
|
\n\
|
||||||
|
-- Insert default configuration values\n\
|
||||||
|
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\
|
||||||
|
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\
|
||||||
|
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\
|
||||||
|
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\
|
||||||
|
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\
|
||||||
|
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\
|
||||||
|
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\
|
||||||
|
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\
|
||||||
|
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\
|
||||||
|
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\
|
||||||
|
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\
|
||||||
|
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\
|
||||||
|
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\
|
||||||
|
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\
|
||||||
|
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\
|
||||||
|
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\
|
||||||
|
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\
|
||||||
|
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\
|
||||||
|
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\
|
||||||
|
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\
|
||||||
|
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\
|
||||||
|
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\
|
||||||
|
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\
|
||||||
|
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\
|
||||||
|
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\
|
||||||
|
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\
|
||||||
|
\n\
|
||||||
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||||
-- Optional database logging for subscription analytics and debugging\n\
|
-- Optional database logging for subscription analytics and debugging\n\
|
||||||
\n\
|
\n\
|
||||||
|
|||||||
191
test_relay.js
191
test_relay.js
@@ -1,191 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Import the nostr-tools bundle
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { TextEncoder, TextDecoder } = require('util');
|
|
||||||
|
|
||||||
// Load nostr.bundle.js
|
|
||||||
const bundlePath = path.join(__dirname, 'api', 'nostr.bundle.js');
|
|
||||||
if (!fs.existsSync(bundlePath)) {
|
|
||||||
console.error('nostr.bundle.js not found at:', bundlePath);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and eval the bundle to get NostrTools
|
|
||||||
const bundleCode = fs.readFileSync(bundlePath, 'utf8');
|
|
||||||
const vm = require('vm');
|
|
||||||
|
|
||||||
// Create a more complete browser-like context
|
|
||||||
const context = {
|
|
||||||
window: {},
|
|
||||||
global: {},
|
|
||||||
console: console,
|
|
||||||
setTimeout: setTimeout,
|
|
||||||
setInterval: setInterval,
|
|
||||||
clearTimeout: clearTimeout,
|
|
||||||
clearInterval: clearInterval,
|
|
||||||
Buffer: Buffer,
|
|
||||||
process: process,
|
|
||||||
require: require,
|
|
||||||
module: module,
|
|
||||||
exports: exports,
|
|
||||||
__dirname: __dirname,
|
|
||||||
__filename: __filename,
|
|
||||||
TextEncoder: TextEncoder,
|
|
||||||
TextDecoder: TextDecoder,
|
|
||||||
crypto: require('crypto'),
|
|
||||||
atob: (str) => Buffer.from(str, 'base64').toString('binary'),
|
|
||||||
btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
|
|
||||||
fetch: require('https').get // Basic polyfill, might need adjustment
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add common browser globals to window
|
|
||||||
context.window.TextEncoder = TextEncoder;
|
|
||||||
context.window.TextDecoder = TextDecoder;
|
|
||||||
context.window.crypto = context.crypto;
|
|
||||||
context.window.atob = context.atob;
|
|
||||||
context.window.btoa = context.btoa;
|
|
||||||
context.window.console = console;
|
|
||||||
context.window.setTimeout = setTimeout;
|
|
||||||
context.window.setInterval = setInterval;
|
|
||||||
context.window.clearTimeout = clearTimeout;
|
|
||||||
context.window.clearInterval = clearInterval;
|
|
||||||
|
|
||||||
// Execute bundle in context
|
|
||||||
vm.createContext(context);
|
|
||||||
try {
|
|
||||||
vm.runInContext(bundleCode, context);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading nostr bundle:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug what's available in the context
|
|
||||||
console.log('Bundle loaded, checking available objects...');
|
|
||||||
console.log('context.window keys:', Object.keys(context.window));
|
|
||||||
console.log('context.global keys:', Object.keys(context.global));
|
|
||||||
|
|
||||||
// Try different ways to access NostrTools
|
|
||||||
let NostrTools = context.window.NostrTools || context.NostrTools || context.global.NostrTools;
|
|
||||||
|
|
||||||
// If still not found, look for other possible exports
|
|
||||||
if (!NostrTools) {
|
|
||||||
console.log('Looking for alternative exports...');
|
|
||||||
|
|
||||||
// Check if it's under a different name
|
|
||||||
const windowKeys = Object.keys(context.window);
|
|
||||||
const possibleExports = windowKeys.filter(key =>
|
|
||||||
key.toLowerCase().includes('nostr') ||
|
|
||||||
key.toLowerCase().includes('tools') ||
|
|
||||||
typeof context.window[key] === 'object'
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Possible nostr-related exports:', possibleExports);
|
|
||||||
|
|
||||||
// Try the first one that looks promising
|
|
||||||
if (possibleExports.length > 0) {
|
|
||||||
NostrTools = context.window[possibleExports[0]];
|
|
||||||
console.log(`Trying ${possibleExports[0]}:`, typeof NostrTools);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NostrTools) {
|
|
||||||
console.error('NostrTools not found in bundle');
|
|
||||||
console.error('Bundle might not be compatible with Node.js or needs different loading approach');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('NostrTools loaded successfully');
|
|
||||||
console.log('Available methods:', Object.keys(NostrTools));
|
|
||||||
|
|
||||||
async function testRelay() {
|
|
||||||
const relayUrl = 'ws://127.0.0.1:8888';
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('\n=== Testing Relay Connection ===');
|
|
||||||
console.log('Relay URL:', relayUrl);
|
|
||||||
|
|
||||||
// Create SimplePool
|
|
||||||
const pool = new NostrTools.SimplePool();
|
|
||||||
console.log('SimplePool created');
|
|
||||||
|
|
||||||
// Test 1: Query for kind 1 events
|
|
||||||
console.log('\n--- Test 1: Kind 1 Events ---');
|
|
||||||
const kind1Events = await pool.querySync([relayUrl], {
|
|
||||||
kinds: [1],
|
|
||||||
limit: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Found ${kind1Events.length} kind 1 events`);
|
|
||||||
kind1Events.forEach((event, index) => {
|
|
||||||
console.log(`Event ${index + 1}:`, {
|
|
||||||
id: event.id,
|
|
||||||
kind: event.kind,
|
|
||||||
pubkey: event.pubkey.substring(0, 16) + '...',
|
|
||||||
created_at: new Date(event.created_at * 1000).toISOString(),
|
|
||||||
content: event.content.substring(0, 50) + (event.content.length > 50 ? '...' : '')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: Query for kind 33334 events (configuration)
|
|
||||||
console.log('\n--- Test 2: Kind 33334 Events (Configuration) ---');
|
|
||||||
const configEvents = await pool.querySync([relayUrl], {
|
|
||||||
kinds: [33334],
|
|
||||||
limit: 10
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Found ${configEvents.length} kind 33334 events`);
|
|
||||||
configEvents.forEach((event, index) => {
|
|
||||||
console.log(`Config Event ${index + 1}:`, {
|
|
||||||
id: event.id,
|
|
||||||
kind: event.kind,
|
|
||||||
pubkey: event.pubkey.substring(0, 16) + '...',
|
|
||||||
created_at: new Date(event.created_at * 1000).toISOString(),
|
|
||||||
tags: event.tags.length,
|
|
||||||
content: event.content
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show some tags
|
|
||||||
if (event.tags.length > 0) {
|
|
||||||
console.log(' Sample tags:');
|
|
||||||
event.tags.slice(0, 5).forEach(tag => {
|
|
||||||
console.log(` ${tag[0]}: ${tag[1] || ''}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Query for any events
|
|
||||||
console.log('\n--- Test 3: Any Events (limit 3) ---');
|
|
||||||
const anyEvents = await pool.querySync([relayUrl], {
|
|
||||||
limit: 3
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Found ${anyEvents.length} total events`);
|
|
||||||
anyEvents.forEach((event, index) => {
|
|
||||||
console.log(`Event ${index + 1}:`, {
|
|
||||||
id: event.id,
|
|
||||||
kind: event.kind,
|
|
||||||
pubkey: event.pubkey.substring(0, 16) + '...',
|
|
||||||
created_at: new Date(event.created_at * 1000).toISOString()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
pool.close([relayUrl]);
|
|
||||||
console.log('\n=== Test Complete ===');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Relay test failed:', error.message);
|
|
||||||
console.error('Stack:', error.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testRelay().then(() => {
|
|
||||||
console.log('Test finished');
|
|
||||||
process.exit(0);
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Test failed:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user