Browse Source

implement auth

master
Silberengel 1 month ago
parent
commit
8a40837d05
  1. 6
      src/lib/services/nostr/config.ts
  2. 301
      src/lib/services/nostr/nostr-client.ts

6
src/lib/services/nostr/config.ts

@ -8,13 +8,15 @@ const DEFAULT_RELAYS = [ @@ -8,13 +8,15 @@ const DEFAULT_RELAYS = [
'wss://nostr21.com',
'wss://nostr.land',
'wss://nostr.sovbit.host',
'wss://orly-relay.imwald.eu'
'wss://orly-relay.imwald.eu',
'wss://nostr.wine'
];
const PROFILE_RELAYS = [
'wss://relay.damus.io',
'wss://aggr.nostr.land',
'wss://profiles.nostr1.com'
'wss://profiles.nostr1.com',
'wss://relay.primal.net'
];
const THREAD_PUBLISH_RELAYS = [

301
src/lib/services/nostr/nostr-client.ts

@ -3,12 +3,13 @@ @@ -3,12 +3,13 @@
* Features: request throttling, batching, rate limiting, efficient caching
*/
import { Relay, type Filter, matchFilter } from 'nostr-tools';
import { Relay, type Filter, matchFilter, type EventTemplate, type VerifiedEvent } from 'nostr-tools';
import { config } from './config.js';
import type { NostrEvent } from '../../types/nostr.js';
import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey } from '../cache/event-cache.js';
import { getDB } from '../cache/indexeddb-store.js';
import { filterEvents, shouldHideEvent } from '../event-filter.js';
import { sessionManager } from '../auth/session-manager.js';
export interface PublishOptions {
relays?: string[];
@ -47,6 +48,9 @@ class NostrClient { @@ -47,6 +48,9 @@ class NostrClient {
private readonly INITIAL_RETRY_DELAY = 5000; // 5 seconds
private readonly MAX_RETRY_DELAY = 300000; // 5 minutes
private readonly MAX_FAILURE_COUNT = 10; // After 10 failures, wait max delay
// Track authenticated relays to avoid re-authenticating
private authenticatedRelays: Set<string> = new Set();
async initialize(): Promise<void> {
if (this.initialized) return;
@ -69,6 +73,61 @@ class NostrClient { @@ -69,6 +73,61 @@ class NostrClient {
this.initialized = true;
}
/**
* Authenticate with a relay using NIP-42 AUTH
* Only call this when the relay has sent an AUTH challenge or an operation failed with auth-required
*/
private async authenticateRelay(relay: Relay, url: string): Promise<boolean> {
// Check if we're already authenticated
if (this.authenticatedRelays.has(url)) {
return true;
}
// Check if user is logged in
const session = sessionManager.getSession();
if (!session) {
console.debug(`[nostr-client] Cannot authenticate with ${url}: user not logged in`);
return false;
}
try {
// Check if relay supports AUTH by checking for auth method
// nostr-tools Relay.auth() will handle the AUTH flow
// Only call this when relay has sent a challenge (handled by nostr-tools)
if (typeof (relay as any).auth === 'function') {
await relay.auth(async (authEvent: EventTemplate): Promise<VerifiedEvent> => {
// Ensure authEvent has required fields
const eventToSign: Omit<NostrEvent, 'sig' | 'id'> = {
pubkey: session.pubkey,
created_at: authEvent.created_at,
kind: authEvent.kind,
tags: authEvent.tags || [],
content: authEvent.content || ''
};
// Sign the auth event using the session signer
const signedEvent = await session.signer(eventToSign);
// Return as VerifiedEvent (nostr-tools will verify the signature)
return signedEvent as VerifiedEvent;
});
this.authenticatedRelays.add(url);
console.debug(`[nostr-client] Successfully authenticated with relay: ${url}`);
return true;
}
} catch (error) {
// Only log if it's not the "no challenge" error (which is expected for relays that don't require auth)
const errorMessage = error instanceof Error ? error.message : String(error);
if (!errorMessage.includes('no challenge was received')) {
console.debug(`[nostr-client] Failed to authenticate with relay ${url}:`, errorMessage);
}
return false;
}
return false;
}
async addRelay(url: string): Promise<void> {
if (this.relays.has(url)) return;
@ -86,6 +145,11 @@ class NostrClient { @@ -86,6 +145,11 @@ class NostrClient {
try {
const relay = await Relay.connect(url);
this.relays.set(url, relay);
// Don't proactively authenticate - only authenticate when:
// 1. Relay sends an AUTH challenge (handled by nostr-tools automatically)
// 2. An operation fails with 'auth-required' error (handled in publish/subscribe methods)
// Clear failure tracking on successful connection
this.failedRelays.delete(url);
// Log successful connection at debug level to reduce console noise
@ -94,8 +158,17 @@ class NostrClient { @@ -94,8 +158,17 @@ class NostrClient {
// Track the failure
const existingFailure = this.failedRelays.get(url) || { lastFailure: 0, retryAfter: this.INITIAL_RETRY_DELAY, failureCount: 0 };
const failureCount = existingFailure.failureCount + 1;
// For connection refused errors, use longer backoff to avoid spam
const errorMessage = error instanceof Error ? error.message : String(error);
const isConnectionRefused = errorMessage.includes('CONNECTION_REFUSED') ||
errorMessage.includes('connection refused') ||
errorMessage.includes('Connection refused');
// Use longer initial delay for connection refused errors
const initialDelay = isConnectionRefused ? this.INITIAL_RETRY_DELAY * 2 : this.INITIAL_RETRY_DELAY;
const retryAfter = Math.min(
this.INITIAL_RETRY_DELAY * Math.pow(2, Math.min(failureCount - 1, 6)), // Exponential backoff, max 2^6 = 64x
initialDelay * Math.pow(2, Math.min(failureCount - 1, 6)), // Exponential backoff, max 2^6 = 64x
this.MAX_RETRY_DELAY
);
@ -105,7 +178,11 @@ class NostrClient { @@ -105,7 +178,11 @@ class NostrClient {
failureCount
});
console.warn(`[nostr-client] Failed to connect to relay ${url} (failure #${failureCount}), will retry after ${Math.round(retryAfter / 1000)}s`);
// Only log at debug level to reduce console noise - connection failures are expected
// Only warn if it's a persistent failure (after several attempts)
if (failureCount > 3) {
console.debug(`[nostr-client] Relay ${url} connection failed (failure #${failureCount}), will retry after ${Math.round(retryAfter / 1000)}s`);
}
throw error;
}
}
@ -119,6 +196,8 @@ class NostrClient { @@ -119,6 +196,8 @@ class NostrClient {
// Ignore
}
this.relays.delete(url);
// Remove from authenticated relays when disconnecting
this.authenticatedRelays.delete(url);
}
}
@ -328,10 +407,32 @@ class NostrClient { @@ -328,10 +407,32 @@ class NostrClient {
await newRelay.publish(event);
results.success.push(url);
} catch (error) {
results.failed.push({
relay: url,
error: error instanceof Error ? error.message : 'Unknown error'
});
// Check if error is auth-required
if (error instanceof Error && error.message.startsWith('auth-required')) {
// Try to authenticate and retry
const authSuccess = await this.authenticateRelay(newRelay, url);
if (authSuccess) {
try {
await newRelay.publish(event);
results.success.push(url);
} catch (retryError) {
results.failed.push({
relay: url,
error: retryError instanceof Error ? retryError.message : 'Unknown error'
});
}
} else {
results.failed.push({
relay: url,
error: 'Authentication required but failed'
});
}
} else {
results.failed.push({
relay: url,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
} catch (error) {
@ -345,10 +446,32 @@ class NostrClient { @@ -345,10 +446,32 @@ class NostrClient {
await relay.publish(event);
results.success.push(url);
} catch (error) {
results.failed.push({
relay: url,
error: error instanceof Error ? error.message : 'Unknown error'
});
// Check if error is auth-required
if (error instanceof Error && error.message.startsWith('auth-required')) {
// Try to authenticate and retry
const authSuccess = await this.authenticateRelay(relay, url);
if (authSuccess) {
try {
await relay.publish(event);
results.success.push(url);
} catch (retryError) {
results.failed.push({
relay: url,
error: retryError instanceof Error ? retryError.message : 'Unknown error'
});
}
} else {
results.failed.push({
relay: url,
error: 'Authentication required but failed'
});
}
} else {
results.failed.push({
relay: url,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
}
@ -402,28 +525,53 @@ class NostrClient { @@ -402,28 +525,53 @@ class NostrClient {
try {
const client = this;
const sub = relay.subscribe(filters, {
onevent: (event: NostrEvent) => {
try {
if (!client.relays.has(url)) return;
if (client.shouldFilterZapReceipt(event)) return;
client.addToCache(event);
onEvent(event, url);
} catch (err) {
// Silently handle errors - connection may be closed
}
},
oneose: () => {
try {
if (!client.relays.has(url)) return;
onEose?.(url);
} catch (err) {
// Silently handle errors - connection may be closed
let hasAuthed = this.authenticatedRelays.has(url);
const startSub = () => {
const sub = relay.subscribe(filters, {
onevent: (event: NostrEvent) => {
try {
if (!client.relays.has(url)) return;
if (client.shouldFilterZapReceipt(event)) return;
client.addToCache(event);
onEvent(event, url);
} catch (err) {
// Silently handle errors - connection may be closed
}
},
oneose: () => {
try {
if (!client.relays.has(url)) return;
onEose?.(url);
} catch (err) {
// Silently handle errors - connection may be closed
}
},
onclose: (reason: string) => {
// Handle auth-required on subscription close
if (reason.startsWith('auth-required') && !hasAuthed) {
const session = sessionManager.getSession();
if (session) {
client.authenticateRelay(relay, url)
.then((authSuccess) => {
if (authSuccess) {
hasAuthed = true;
// Retry subscription after authentication
startSub();
}
})
.catch(() => {
// Authentication failed, give up
});
}
}
}
}
});
});
this.subscriptions.set(`${url}_${subId}`, { relay, sub });
client.subscriptions.set(`${url}_${subId}`, { relay, sub });
};
startSub();
} catch (error) {
// Handle errors
}
@ -511,6 +659,7 @@ class NostrClient { @@ -511,6 +659,7 @@ class NostrClient {
const subId = `sub_${this.nextSubId++}_${Date.now()}`;
let resolved = false;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let hasAuthed = this.authenticatedRelays.has(relayUrl);
const finish = () => {
if (resolved) return;
@ -519,37 +668,66 @@ class NostrClient { @@ -519,37 +668,66 @@ class NostrClient {
this.unsubscribe(subId);
};
try {
const client = this;
const sub = relay.subscribe(filters, {
onevent: (event: NostrEvent) => {
try {
if (!client.relays.has(relayUrl)) return;
if (shouldHideEvent(event)) return;
if (client.shouldFilterZapReceipt(event)) return;
events.set(event.id, event);
client.addToCache(event);
} catch (error) {
// Silently handle errors - connection may be closed
}
},
oneose: () => {
try {
if (!resolved) finish();
} catch (error) {
// Silently handle errors - connection may be closed
const startSub = () => {
try {
const client = this;
const sub = relay.subscribe(filters, {
onevent: (event: NostrEvent) => {
try {
if (!client.relays.has(relayUrl)) return;
if (shouldHideEvent(event)) return;
if (client.shouldFilterZapReceipt(event)) return;
events.set(event.id, event);
client.addToCache(event);
} catch (error) {
// Silently handle errors - connection may be closed
}
},
oneose: () => {
try {
if (!resolved) finish();
} catch (error) {
// Silently handle errors - connection may be closed
}
},
onclose: (reason: string) => {
// Handle auth-required on subscription close
if (reason.startsWith('auth-required') && !hasAuthed && !resolved) {
const session = sessionManager.getSession();
if (session) {
client.authenticateRelay(relay, relayUrl)
.then((authSuccess) => {
if (authSuccess) {
hasAuthed = true;
// Retry subscription after authentication
if (!resolved) {
startSub();
}
} else {
finish();
}
})
.catch(() => {
finish();
});
} else {
finish();
}
}
}
}
});
});
this.subscriptions.set(`${relayUrl}_${subId}`, { relay, sub });
client.subscriptions.set(`${relayUrl}_${subId}`, { relay, sub });
timeoutId = setTimeout(() => {
if (!resolved) finish();
}, timeout);
} catch (error) {
finish();
}
timeoutId = setTimeout(() => {
if (!resolved) finish();
}, timeout);
} catch (error) {
finish();
}
};
startSub();
}
private processQueue(): void {
@ -672,7 +850,10 @@ class NostrClient { @@ -672,7 +850,10 @@ class NostrClient {
return [];
}
console.debug(`[nostr-client] Fetching from ${connectedRelays.length} connected relay(s) out of ${relays.length} requested`);
// Only log if we're missing a significant number of relays
if (connectedRelays.length < relays.length * 0.5) {
console.debug(`[nostr-client] Fetching from ${connectedRelays.length} connected relay(s) out of ${relays.length} requested`);
}
// Process relays sequentially with throttling to avoid overload
const events: Map<string, NostrEvent> = new Map();

Loading…
Cancel
Save