clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

609 lines
18 KiB

import NDK, {
NDKNip07Signer,
NDKRelay,
NDKRelayAuthPolicies,
NDKRelaySet,
NDKUser,
NDKEvent,
} from "@nostr-dev-kit/ndk";
import { get, writable, type Writable } from "svelte/store";
import {
secondaryRelays,
FeedType,
loginStorageKey,
communityRelays,
anonymousRelays,
searchRelays,
} from "./consts";
import {
buildCompleteRelaySet,
testRelayConnection,
discoverLocalRelays,
getUserLocalRelays,
getUserBlockedRelays,
getUserOutboxRelays,
deduplicateRelayUrls,
} from "./utils/relay_management";
// Re-export testRelayConnection for components that need it
export { testRelayConnection };
import { startNetworkMonitoring, NetworkCondition } from "./utils/network_detection";
import { userStore } from "./stores/userStore";
import { userPubkey } from "$lib/stores/authStore.Svelte";
import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring } from "./stores/networkStore";
export const ndkInstance: Writable<NDK> = writable();
export const ndkSignedIn = writable(false);
export const activePubkey = writable<string | null>(null);
export const inboxRelays = writable<string[]>([]);
export const outboxRelays = writable<string[]>([]);
// New relay management stores
export const activeInboxRelays = writable<string[]>([]);
export const activeOutboxRelays = writable<string[]>([]);
/**
* Custom authentication policy that handles NIP-42 authentication manually
* when the default NDK authentication fails
*/
class CustomRelayAuthPolicy {
private ndk: NDK;
private challenges: Map<string, string> = new Map();
constructor(ndk: NDK) {
this.ndk = ndk;
}
/**
* Handles authentication for a relay
* @param relay The relay to authenticate with
* @returns Promise that resolves when authentication is complete
*/
async authenticate(relay: NDKRelay): Promise<void> {
if (!this.ndk.signer || !this.ndk.activeUser) {
console.warn(
"[NDK.ts] No signer or active user available for relay authentication",
);
return;
}
try {
console.debug(`[NDK.ts] Setting up authentication for ${relay.url}`);
// Listen for AUTH challenges
relay.on("auth", (challenge: string) => {
console.debug(
`[NDK.ts] Received AUTH challenge from ${relay.url}:`,
challenge,
);
this.challenges.set(relay.url, challenge);
this.handleAuthChallenge(relay, challenge);
});
// Listen for auth-required errors (handle via notice events)
relay.on("notice", (message: string) => {
if (message.includes("auth-required")) {
console.debug(`[NDK.ts] Auth required from ${relay.url}:`, message);
this.handleAuthRequired(relay, message);
}
});
// Listen for successful authentication
relay.on("authed", () => {
console.debug(`[NDK.ts] Successfully authenticated to ${relay.url}`);
});
// Listen for authentication failures
relay.on("auth:failed", (error: any) => {
console.error(
`[NDK.ts] Authentication failed for ${relay.url}:`,
error,
);
});
} catch (error) {
console.error(
`[NDK.ts] Error setting up authentication for ${relay.url}:`,
error,
);
}
}
/**
* Handles AUTH challenge from relay
*/
private async handleAuthChallenge(
relay: NDKRelay,
challenge: string,
): Promise<void> {
try {
if (!this.ndk.signer || !this.ndk.activeUser) {
console.warn("[NDK.ts] No signer available for AUTH challenge");
return;
}
// Create NIP-42 authentication event
const authEvent = {
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
tags: [
["relay", relay.url],
["challenge", challenge],
],
content: "",
pubkey: this.ndk.activeUser.pubkey,
};
// Create and sign the authentication event using NDKEvent
const authNDKEvent = new NDKEvent(this.ndk, authEvent);
await authNDKEvent.sign();
// Send AUTH message to relay using the relay's publish method
await relay.publish(authNDKEvent);
console.debug(`[NDK.ts] Sent AUTH to ${relay.url}`);
} catch (error) {
console.error(
`[NDK.ts] Error handling AUTH challenge for ${relay.url}:`,
error,
);
}
}
/**
* Handles auth-required error from relay
*/
private async handleAuthRequired(
relay: NDKRelay,
message: string,
): Promise<void> {
const challenge = this.challenges.get(relay.url);
if (challenge) {
await this.handleAuthChallenge(relay, challenge);
} else {
console.warn(
`[NDK.ts] Auth required from ${relay.url} but no challenge available`,
);
}
}
}
/**
* Checks if the current environment might cause WebSocket protocol downgrade
*/
export function checkEnvironmentForWebSocketDowngrade(): void {
console.debug("[NDK.ts] Environment Check for WebSocket Protocol:");
const isLocalhost =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
const isHttp = window.location.protocol === "http:";
const isHttps = window.location.protocol === "https:";
console.debug("[NDK.ts] - Is localhost:", isLocalhost);
console.debug("[NDK.ts] - Protocol:", window.location.protocol);
console.debug("[NDK.ts] - Is HTTP:", isHttp);
console.debug("[NDK.ts] - Is HTTPS:", isHttps);
if (isLocalhost && isHttp) {
console.warn(
"[NDK.ts] ⚠ Running on localhost with HTTP - WebSocket downgrade to ws:// is expected",
);
console.warn("[NDK.ts] This is normal for development environments");
} else if (isHttp) {
console.error(
"[NDK.ts] ❌ Running on HTTP - WebSocket connections will be insecure",
);
console.error("[NDK.ts] Consider using HTTPS in production");
} else if (isHttps) {
console.debug(
"[NDK.ts] ✓ Running on HTTPS - Secure WebSocket connections should work",
);
}
}
/**
* Checks WebSocket protocol support and logs diagnostic information
*/
export function checkWebSocketSupport(): void {
console.debug("[NDK.ts] WebSocket Support Diagnostics:");
console.debug("[NDK.ts] - Protocol:", window.location.protocol);
console.debug("[NDK.ts] - Hostname:", window.location.hostname);
console.debug("[NDK.ts] - Port:", window.location.port);
console.debug("[NDK.ts] - User Agent:", navigator.userAgent);
// Test if secure WebSocket is supported
try {
const testWs = new WebSocket("wss://echo.websocket.org");
testWs.onopen = () => {
console.debug("[NDK.ts] ✓ Secure WebSocket (wss://) is supported");
testWs.close();
};
testWs.onerror = () => {
console.warn("[NDK.ts] ✗ Secure WebSocket (wss://) may not be supported");
};
} catch (error) {
console.warn("[NDK.ts] ✗ WebSocket test failed:", error);
}
}
/**
* Gets the user's pubkey from local storage, if it exists.
* @returns The user's pubkey, or null if there is no logged-in user.
* @remarks Local storage is used in place of cookies to persist the user's login across browser
* sessions.
*/
export function getPersistedLogin(): string | null {
const pubkey = localStorage.getItem(loginStorageKey);
return pubkey;
}
/**
* Writes the user's pubkey to local storage.
* @param user The user to persist.
* @remarks Use this function when the user logs in. Currently, only one pubkey is stored at a
* time.
*/
export function persistLogin(user: NDKUser): void {
localStorage.setItem(loginStorageKey, user.pubkey);
}
/**
* Clears the user's pubkey from local storage.
* @remarks Use this function when the user logs out.
*/
export function clearLogin(): void {
localStorage.removeItem(loginStorageKey);
}
/**
* Constructs a key use to designate a user's relay lists in local storage.
* @param user The user for whom to construct the key.
* @param type The type of relay list to designate.
* @returns The constructed key.
*/
function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string {
return `${loginStorageKey}/${user.pubkey}/${type}`;
}
/**
* Stores the user's relay lists in local storage.
* @param user The user for whom to store the relay lists.
* @param inboxes The user's inbox relays.
* @param outboxes The user's outbox relays.
*/
function persistRelays(
user: NDKUser,
inboxes: Set<NDKRelay>,
outboxes: Set<NDKRelay>,
): void {
localStorage.setItem(
getRelayStorageKey(user, "inbox"),
JSON.stringify(Array.from(inboxes).map((relay) => relay.url)),
);
localStorage.setItem(
getRelayStorageKey(user, "outbox"),
JSON.stringify(Array.from(outboxes).map((relay) => relay.url)),
);
}
/**
* Retrieves the user's relay lists from local storage.
* @param user The user for whom to retrieve the relay lists.
* @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`. Either set may be
* empty if no relay lists were stored for the user.
*/
function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] {
const inboxes = new Set<string>(
JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"),
);
const outboxes = new Set<string>(
JSON.parse(
localStorage.getItem(getRelayStorageKey(user, "outbox")) ?? "[]",
),
);
return [inboxes, outboxes];
}
export function clearPersistedRelays(user: NDKUser): void {
localStorage.removeItem(getRelayStorageKey(user, "inbox"));
localStorage.removeItem(getRelayStorageKey(user, "outbox"));
}
/**
* Ensures a relay URL uses secure WebSocket protocol
* @param url The relay URL to secure
* @returns The URL with wss:// protocol
*/
function ensureSecureWebSocket(url: string): string {
// Replace ws:// with wss:// if present
const secureUrl = url.replace(/^ws:\/\//, "wss://");
if (secureUrl !== url) {
console.warn(
`[NDK.ts] Protocol downgrade detected: ${url} -> ${secureUrl}`,
);
}
return secureUrl;
}
/**
* Creates a relay with proper authentication handling
*/
function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
console.debug(`[NDK.ts] Creating relay with URL: ${url}`);
// Ensure the URL is using wss:// protocol
const secureUrl = ensureSecureWebSocket(url);
// Add connection timeout and error handling
const relay = new NDKRelay(
secureUrl,
NDKRelayAuthPolicies.signIn({ ndk }),
ndk,
);
// Set up connection timeout
const connectionTimeout = setTimeout(() => {
console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`);
relay.disconnect();
}, 10000); // 10 second timeout
// Set up custom authentication handling only if user is signed in
if (ndk.signer && ndk.activeUser) {
const authPolicy = new CustomRelayAuthPolicy(ndk);
relay.on("connect", () => {
console.debug(`[NDK.ts] Relay connected: ${secureUrl}`);
clearTimeout(connectionTimeout);
authPolicy.authenticate(relay);
});
} else {
relay.on("connect", () => {
console.debug(`[NDK.ts] Relay connected: ${secureUrl}`);
clearTimeout(connectionTimeout);
});
}
// Add error handling
relay.on("disconnect", () => {
console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`);
clearTimeout(connectionTimeout);
});
return relay;
}
/**
* Gets the active relay set for the current user
* @param ndk NDK instance
* @returns Promise that resolves to object with inbox and outbox relay arrays
*/
export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> {
const user = get(userStore);
if (user.signedIn && user.ndkUser) {
return await buildCompleteRelaySet(ndk, user.ndkUser);
} else {
return await buildCompleteRelaySet(ndk, null);
}
}
/**
* Updates the active relay stores and NDK pool with new relay URLs
* @param ndk NDK instance
*/
export async function updateActiveRelayStores(ndk: NDK): Promise<void> {
try {
// Get the active relay set from the relay management system
const relaySet = await getActiveRelaySet(ndk);
// Update the stores with the new relay configuration
activeInboxRelays.set(relaySet.inboxRelays);
activeOutboxRelays.set(relaySet.outboxRelays);
// Add relays to NDK pool (deduplicated)
const allRelayUrls = deduplicateRelayUrls([...relaySet.inboxRelays, ...relaySet.outboxRelays]);
for (const url of allRelayUrls) {
try {
const relay = createRelayWithAuth(url, ndk);
ndk.pool?.addRelay(relay);
} catch (error) {
// Silently ignore relay addition failures
}
}
} catch (error) {
// Silently ignore relay store update errors
}
}
/**
* Logs the current relay configuration to console
*/
export function logCurrentRelayConfiguration(): void {
const inboxRelays = get(activeInboxRelays);
const outboxRelays = get(activeOutboxRelays);
console.log('🔌 Current Relay Configuration:');
console.log('📥 Inbox Relays:', inboxRelays);
console.log('📤 Outbox Relays:', outboxRelays);
console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`);
}
/**
* Updates relay stores when user state changes
* @param ndk NDK instance
*/
export async function refreshRelayStores(ndk: NDK): Promise<void> {
console.debug('[NDK.ts] Refreshing relay stores due to user state change');
await updateActiveRelayStores(ndk);
}
/**
* Updates relay stores when network condition changes
* @param ndk NDK instance
*/
export async function refreshRelayStoresOnNetworkChange(ndk: NDK): Promise<void> {
console.debug('[NDK.ts] Refreshing relay stores due to network condition change');
await updateActiveRelayStores(ndk);
}
/**
* Starts network monitoring for relay optimization
* @param ndk NDK instance
*/
export function startNetworkMonitoringForRelays(ndk: NDK): void {
// Use centralized network monitoring instead of separate monitoring
startNetworkStatusMonitoring();
}
/**
* Creates NDKRelaySet from relay URLs with proper authentication
* @param relayUrls Array of relay URLs
* @param ndk NDK instance
* @returns NDKRelaySet
*/
function createRelaySetFromUrls(relayUrls: string[], ndk: NDK): NDKRelaySet {
const relays = relayUrls.map(url =>
new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk)
);
return new NDKRelaySet(new Set(relays), ndk);
}
/**
* Gets the active relay set as NDKRelaySet for use in queries
* @param ndk NDK instance
* @param useInbox Whether to use inbox relays (true) or outbox relays (false)
* @returns Promise that resolves to NDKRelaySet
*/
export async function getActiveRelaySetAsNDKRelaySet(
ndk: NDK,
useInbox: boolean = true
): Promise<NDKRelaySet> {
const relaySet = await getActiveRelaySet(ndk);
const urls = useInbox ? relaySet.inboxRelays : relaySet.outboxRelays;
return createRelaySetFromUrls(urls, ndk);
}
/**
* Initializes an instance of NDK with the new relay management system
* @returns The initialized NDK instance
*/
export function initNdk(): NDK {
console.debug("[NDK.ts] Initializing NDK with new relay management system");
const ndk = new NDK({
autoConnectUserRelays: false, // We'll manage relays manually
enableOutboxModel: true,
});
// Set up custom authentication policy
ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk });
// Connect with better error handling and reduced retry attempts
let retryCount = 0;
const maxRetries = 2;
const attemptConnection = async () => {
try {
await ndk.connect();
console.debug("[NDK.ts] NDK connected successfully");
// Update relay stores after connection
await updateActiveRelayStores(ndk);
// Start network monitoring for relay optimization
startNetworkMonitoringForRelays(ndk);
} catch (error) {
console.warn("[NDK.ts] Failed to connect NDK:", error);
// Only retry a limited number of times
if (retryCount < maxRetries) {
retryCount++;
console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`);
setTimeout(attemptConnection, 3000);
} else {
console.warn("[NDK.ts] Max retries reached, continuing with limited functionality");
// Still try to update relay stores even if connection failed
try {
await updateActiveRelayStores(ndk);
startNetworkMonitoringForRelays(ndk);
} catch (storeError) {
console.warn("[NDK.ts] Failed to update relay stores:", storeError);
}
}
}
};
attemptConnection();
return ndk;
}
/**
* Signs in with a NIP-07 browser extension using the new relay management system
* @returns The user's profile, if it is available
* @throws If sign-in fails
*/
export async function loginWithExtension(
pubkey?: string,
): Promise<NDKUser | null> {
try {
const ndk = get(ndkInstance);
const signer = new NDKNip07Signer();
const signerUser = await signer.user();
// TODO: Handle changing pubkeys.
if (pubkey && signerUser.pubkey !== pubkey) {
console.debug("[NDK.ts] Switching pubkeys from last login.");
}
activePubkey.set(signerUser.pubkey);
userPubkey.set(signerUser.pubkey);
const user = ndk.getUser({ pubkey: signerUser.pubkey });
// Update relay stores with the new system
await updateActiveRelayStores(ndk);
ndk.signer = signer;
ndk.activeUser = user;
ndkInstance.set(ndk);
ndkSignedIn.set(true);
return user;
} catch (e) {
throw new Error(`Failed to sign in with NIP-07 extension: ${e}`);
}
}
/**
* Handles logging out a user.
* @param user The user to log out.
*/
export function logout(user: NDKUser): void {
clearLogin();
clearPersistedRelays(user);
activePubkey.set(null);
userPubkey.set(null);
ndkSignedIn.set(false);
// Clear relay stores
activeInboxRelays.set([]);
activeOutboxRelays.set([]);
// Stop network monitoring
stopNetworkStatusMonitoring();
// Re-initialize with anonymous instance
const newNdk = initNdk();
ndkInstance.set(newNdk);
}