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.
 
 
 
 

301 lines
8.9 KiB

import { writable, get } from 'svelte/store';
import type { NostrProfile } from '$lib/utils/nostrUtils';
import type { NDKUser, NDKSigner } from '@nostr-dev-kit/ndk';
import { NDKNip07Signer, NDKRelayAuthPolicies, NDKRelaySet, NDKRelay } from '@nostr-dev-kit/ndk';
import { getUserMetadata } from '$lib/utils/nostrUtils';
import { ndkInstance } from '$lib/ndk';
import { loginStorageKey, fallbackRelays } from '$lib/consts';
import { nip19 } from 'nostr-tools';
export interface UserState {
pubkey: string | null;
npub: string | null;
profile: NostrProfile | null;
relays: { inbox: string[]; outbox: string[] };
loginMethod: 'extension' | 'amber' | 'npub' | null;
ndkUser: NDKUser | null;
signer: NDKSigner | null;
signedIn: boolean;
}
export const userStore = writable<UserState>({
pubkey: null,
npub: null,
profile: null,
relays: { inbox: [], outbox: [] },
loginMethod: null,
ndkUser: null,
signer: null,
signedIn: false,
});
// Helper functions for relay management
function getRelayStorageKey(user: NDKUser, type: 'inbox' | 'outbox'): string {
return `${loginStorageKey}/${user.pubkey}/${type}`;
}
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))
);
}
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];
}
async function getUserPreferredRelays(
ndk: any,
user: NDKUser,
fallbacks: readonly string[] = fallbackRelays
): Promise<[Set<NDKRelay>, Set<NDKRelay>]> {
const relayList = await ndk.fetchEvent(
{
kinds: [10002],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
},
NDKRelaySet.fromRelayUrls(fallbacks, ndk),
);
const inboxRelays = new Set<NDKRelay>();
const outboxRelays = new Set<NDKRelay>();
if (relayList == null) {
const relayMap = await window.nostr?.getRelays?.();
Object.entries(relayMap ?? {}).forEach(([url, relayType]: [string, any]) => {
const relay = new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk);
if (relayType.read) inboxRelays.add(relay);
if (relayType.write) outboxRelays.add(relay);
});
} else {
relayList.tags.forEach((tag: string[]) => {
switch (tag[0]) {
case 'r':
inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
break;
case 'w':
outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
break;
default:
inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
break;
}
});
}
return [inboxRelays, outboxRelays];
}
// --- Unified login/logout helpers ---
export const loginMethodStorageKey = 'alexandria/login/method';
function persistLogin(user: NDKUser, method: 'extension' | 'amber' | 'npub') {
localStorage.setItem(loginStorageKey, user.pubkey);
localStorage.setItem(loginMethodStorageKey, method);
}
function getPersistedLoginMethod(): 'extension' | 'amber' | 'npub' | null {
return (localStorage.getItem(loginMethodStorageKey) as 'extension' | 'amber' | 'npub') ?? null;
}
function clearLogin() {
localStorage.removeItem(loginStorageKey);
localStorage.removeItem(loginMethodStorageKey);
}
/**
* Login with NIP-07 browser extension
*/
export async function loginWithExtension() {
const ndk = get(ndkInstance);
if (!ndk) throw new Error('NDK not initialized');
// Only clear previous login state after successful login
const signer = new NDKNip07Signer();
const user = await signer.user();
const npub = user.npub;
const profile = await getUserMetadata(npub);
// Fetch user's preferred relays
const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user);
for (const relay of persistedInboxes) {
ndk.addExplicitRelay(relay);
}
const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user);
persistRelays(user, inboxes, outboxes);
ndk.signer = signer;
ndk.activeUser = user;
userStore.set({
pubkey: user.pubkey,
npub,
profile,
relays: {
inbox: Array.from(inboxes ?? persistedInboxes).map(relay => relay.url),
outbox: Array.from(outboxes ?? persistedOutboxes).map(relay => relay.url)
},
loginMethod: 'extension',
ndkUser: user,
signer,
signedIn: true,
});
clearLogin();
localStorage.removeItem('alexandria/logout/flag');
persistLogin(user, 'extension');
}
/**
* Login with Amber (NIP-46)
*/
export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) {
const ndk = get(ndkInstance);
if (!ndk) throw new Error('NDK not initialized');
// Only clear previous login state after successful login
const npub = user.npub;
const profile = await getUserMetadata(npub, true); // Force fresh fetch
const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user);
for (const relay of persistedInboxes) {
ndk.addExplicitRelay(relay);
}
const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user);
persistRelays(user, inboxes, outboxes);
ndk.signer = amberSigner;
ndk.activeUser = user;
userStore.set({
pubkey: user.pubkey,
npub,
profile,
relays: {
inbox: Array.from(inboxes ?? persistedInboxes).map(relay => relay.url),
outbox: Array.from(outboxes ?? persistedOutboxes).map(relay => relay.url)
},
loginMethod: 'amber',
ndkUser: user,
signer: amberSigner,
signedIn: true,
});
clearLogin();
localStorage.removeItem('alexandria/logout/flag');
persistLogin(user, 'amber');
}
/**
* Login with npub (read-only)
*/
export async function loginWithNpub(pubkeyOrNpub: string) {
const ndk = get(ndkInstance);
if (!ndk) throw new Error('NDK not initialized');
// Only clear previous login state after successful login
let hexPubkey: string;
if (pubkeyOrNpub.startsWith('npub')) {
try {
hexPubkey = nip19.decode(pubkeyOrNpub).data as string;
} catch (e) {
console.error('Failed to decode hex pubkey from npub:', pubkeyOrNpub, e);
throw e;
}
} else {
hexPubkey = pubkeyOrNpub;
}
let npub: string;
try {
npub = nip19.npubEncode(hexPubkey);
} catch (e) {
console.error('Failed to encode npub from hex pubkey:', hexPubkey, e);
throw e;
}
const user = ndk.getUser({ npub });
const profile = await getUserMetadata(npub);
ndk.signer = undefined;
ndk.activeUser = user;
userStore.set({
pubkey: user.pubkey,
npub,
profile,
relays: { inbox: [], outbox: [] },
loginMethod: 'npub',
ndkUser: user,
signer: null,
signedIn: true,
});
clearLogin();
localStorage.removeItem('alexandria/logout/flag');
persistLogin(user, 'npub');
}
/**
* Logout and clear all user state
*/
export function logoutUser() {
console.log('Logging out user...');
const currentUser = get(userStore);
if (currentUser.ndkUser) {
// Clear persisted relays for the user
localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, 'inbox'));
localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, 'outbox'));
}
// Clear all possible login states from localStorage
clearLogin();
// Also clear any other potential login keys that might exist
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (key.includes('login') || key.includes('nostr') || key.includes('user') || key.includes('alexandria') || key === 'pubkey')) {
keysToRemove.push(key);
}
}
// Specifically target the login storage key
keysToRemove.push('alexandria/login/pubkey');
keysToRemove.push('alexandria/login/method');
keysToRemove.forEach(key => {
console.log('Removing localStorage key:', key);
localStorage.removeItem(key);
});
// Clear Amber-specific flags
localStorage.removeItem('alexandria/amber/fallback');
// Set a flag to prevent auto-login on next page load
localStorage.setItem('alexandria/logout/flag', 'true');
console.log('Cleared all login data from localStorage');
userStore.set({
pubkey: null,
npub: null,
profile: null,
relays: { inbox: [], outbox: [] },
loginMethod: null,
ndkUser: null,
signer: null,
signedIn: false,
});
const ndk = get(ndkInstance);
if (ndk) {
ndk.activeUser = undefined;
ndk.signer = undefined;
}
console.log('Logout complete');
}