2025-04-04 15:46:31 -04:00
|
|
|
import NDK, { NDKUser, NDKEvent, NDKSigner } from '@nostr-dev-kit/ndk-mobile';
|
|
|
|
import * as SecureStore from 'expo-secure-store';
|
|
|
|
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk-mobile';
|
|
|
|
import { NDKAmberSigner } from '../signers/NDKAmberSigner';
|
|
|
|
import { generateId, generateDTag } from '@/utils/ids';
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
import { AuthMethod } from './types';
|
2025-04-02 23:40:54 -04:00
|
|
|
|
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Auth Service for managing authentication with NDK and React Query
|
|
|
|
*
|
|
|
|
* Provides functionality for:
|
|
|
|
* - Login with private key
|
|
|
|
* - Login with Amber external signer
|
|
|
|
* - Ephemeral key generation
|
|
|
|
* - Secure credential storage
|
|
|
|
* - Logout and cleanup
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
|
|
|
export class AuthService {
|
|
|
|
private ndk: NDK;
|
2025-04-04 15:46:31 -04:00
|
|
|
private initialized: boolean = false;
|
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
constructor(ndk: NDK) {
|
|
|
|
this.ndk = ndk;
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Initialize the auth service
|
|
|
|
* This is called automatically by the ReactQueryAuthProvider
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
|
|
|
async initialize(): Promise<void> {
|
2025-04-04 15:46:31 -04:00
|
|
|
if (this.initialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Check if we have credentials stored
|
|
|
|
const privateKey = await SecureStore.getItemAsync('powr.private_key');
|
|
|
|
const externalSignerJson = await SecureStore.getItemAsync('nostr_external_signer');
|
|
|
|
|
|
|
|
// Login with stored credentials if available
|
2025-04-02 23:40:54 -04:00
|
|
|
if (privateKey) {
|
|
|
|
await this.loginWithPrivateKey(privateKey);
|
2025-04-04 15:46:31 -04:00
|
|
|
} else if (externalSignerJson) {
|
|
|
|
const { method, data } = JSON.parse(externalSignerJson);
|
|
|
|
if (method === 'amber') {
|
|
|
|
await this.restoreAmberSigner(data);
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
|
|
|
this.initialized = true;
|
2025-04-02 23:40:54 -04:00
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
console.error('[AuthService] Error initializing auth service:', error);
|
|
|
|
throw error;
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
/**
|
|
|
|
* Login with a private key
|
2025-04-04 15:46:31 -04:00
|
|
|
* @param privateKey hex private key
|
|
|
|
* @returns NDK user
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
|
|
|
async loginWithPrivateKey(privateKey: string): Promise<NDKUser> {
|
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Create signer
|
|
|
|
const signer = new NDKPrivateKeySigner(privateKey);
|
|
|
|
this.ndk.signer = signer;
|
2025-04-02 23:40:54 -04:00
|
|
|
|
|
|
|
// Get user
|
2025-04-04 15:46:31 -04:00
|
|
|
await this.ndk.connect();
|
|
|
|
if (!this.ndk.activeUser) {
|
|
|
|
throw new Error('Failed to set active user after login');
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
|
|
|
// Persist the key securely
|
|
|
|
await SecureStore.setItemAsync('powr.private_key', privateKey);
|
|
|
|
|
|
|
|
return this.ndk.activeUser;
|
2025-04-02 23:40:54 -04:00
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
console.error('[AuthService] Error logging in with private key:', error);
|
2025-04-02 23:40:54 -04:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Login with Amber external signer
|
|
|
|
* @returns NDK user
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
2025-04-04 15:46:31 -04:00
|
|
|
async loginWithAmber(): Promise<NDKUser> {
|
2025-04-02 23:40:54 -04:00
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Request public key from Amber
|
|
|
|
const { pubkey, packageName } = await NDKAmberSigner.requestPublicKey();
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Create Amber signer
|
|
|
|
const amberSigner = new NDKAmberSigner(pubkey, packageName);
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Set as NDK signer
|
|
|
|
this.ndk.signer = amberSigner;
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Connect and get user
|
|
|
|
await this.ndk.connect();
|
|
|
|
if (!this.ndk.activeUser) {
|
|
|
|
throw new Error('Failed to set active user after amber login');
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Store the signer info
|
|
|
|
const signerData = {
|
|
|
|
pubkey: pubkey,
|
|
|
|
packageName: packageName
|
|
|
|
};
|
|
|
|
const externalSignerInfo = JSON.stringify({
|
|
|
|
method: 'amber',
|
|
|
|
data: signerData
|
2025-04-02 23:40:54 -04:00
|
|
|
});
|
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
await SecureStore.setItemAsync('nostr_external_signer', externalSignerInfo);
|
|
|
|
await SecureStore.deleteItemAsync('powr.private_key'); // Clear any stored private key
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
return this.ndk.activeUser;
|
2025-04-02 23:40:54 -04:00
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
console.error('[AuthService] Error logging in with Amber:', error);
|
2025-04-02 23:40:54 -04:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Restore an Amber signer session
|
|
|
|
* @param signerData Previous signer data
|
|
|
|
* @returns NDK user
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
2025-04-04 15:46:31 -04:00
|
|
|
private async restoreAmberSigner(signerData: any): Promise<NDKUser> {
|
2025-04-02 23:40:54 -04:00
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Create Amber signer with existing data
|
|
|
|
const amberSigner = new NDKAmberSigner(signerData.pubkey, signerData.packageName);
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Set as NDK signer
|
|
|
|
this.ndk.signer = amberSigner;
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Connect and get user
|
|
|
|
await this.ndk.connect();
|
|
|
|
if (!this.ndk.activeUser) {
|
|
|
|
throw new Error('Failed to set active user after amber signer restore');
|
|
|
|
}
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
return this.ndk.activeUser;
|
2025-04-02 23:40:54 -04:00
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
console.error('[AuthService] Error restoring Amber signer:', error);
|
2025-04-02 23:40:54 -04:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Create an ephemeral key for temporary use
|
|
|
|
* @returns NDK user
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
2025-04-04 15:46:31 -04:00
|
|
|
async createEphemeralKey(): Promise<NDKUser> {
|
2025-04-02 23:40:54 -04:00
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Generate a random key (not persisted)
|
|
|
|
// This creates a hex string of 64 characters (32 bytes)
|
|
|
|
// Use uuidv4 to generate random bytes
|
|
|
|
const randomId = uuidv4().replace(/-/g, '') + uuidv4().replace(/-/g, '');
|
|
|
|
const privateKey = randomId.substring(0, 64); // Ensure exactly 64 hex chars (32 bytes)
|
|
|
|
const signer = new NDKPrivateKeySigner(privateKey);
|
|
|
|
|
|
|
|
// Set as NDK signer
|
|
|
|
this.ndk.signer = signer;
|
|
|
|
|
|
|
|
// Connect and get user
|
|
|
|
await this.ndk.connect();
|
|
|
|
if (!this.ndk.activeUser) {
|
|
|
|
throw new Error('Failed to set active user after ephemeral key creation');
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Clear any stored credentials
|
|
|
|
await SecureStore.deleteItemAsync('powr.private_key');
|
|
|
|
await SecureStore.deleteItemAsync('nostr_external_signer');
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
return this.ndk.activeUser;
|
2025-04-02 23:40:54 -04:00
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
console.error('[AuthService] Error creating ephemeral key:', error);
|
2025-04-02 23:40:54 -04:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Log out the current user
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
2025-04-04 15:46:31 -04:00
|
|
|
async logout(): Promise<void> {
|
|
|
|
try {
|
|
|
|
// Clear stored credentials
|
|
|
|
await SecureStore.deleteItemAsync('powr.private_key');
|
|
|
|
await SecureStore.deleteItemAsync('nostr_external_signer');
|
|
|
|
|
|
|
|
// Reset NDK
|
|
|
|
this.ndk.signer = undefined;
|
|
|
|
|
|
|
|
// Simple cleanup for NDK instance
|
|
|
|
// NDK doesn't have a formal disconnect method
|
2025-04-02 23:40:54 -04:00
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Clean up relay connections if they exist
|
|
|
|
if (this.ndk.pool) {
|
|
|
|
// Cast to any to bypass TypeScript errors with internal NDK API
|
|
|
|
const pool = this.ndk.pool as any;
|
|
|
|
if (pool.relayByUrl) {
|
|
|
|
Object.values(pool.relayByUrl).forEach((relay: any) => {
|
|
|
|
try {
|
|
|
|
if (relay && relay.close) relay.close();
|
|
|
|
} catch (e) {
|
|
|
|
console.warn('Error closing relay:', e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.warn('Error during NDK resource cleanup:', e);
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
|
|
|
console.log('[AuthService] Logged out successfully');
|
|
|
|
} catch (error) {
|
|
|
|
console.error('[AuthService] Error during logout:', error);
|
|
|
|
throw error;
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-02 23:40:54 -04:00
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Get the current authentication method
|
|
|
|
* @returns Auth method or undefined if not authenticated
|
2025-04-02 23:40:54 -04:00
|
|
|
*/
|
2025-04-04 15:46:31 -04:00
|
|
|
async getCurrentAuthMethod(): Promise<AuthMethod | undefined> {
|
2025-04-02 23:40:54 -04:00
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
if (await SecureStore.getItemAsync('powr.private_key')) {
|
|
|
|
return 'private_key';
|
|
|
|
}
|
2025-04-02 23:40:54 -04:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
const externalSignerJson = await SecureStore.getItemAsync('nostr_external_signer');
|
|
|
|
if (externalSignerJson) {
|
|
|
|
const { method } = JSON.parse(externalSignerJson);
|
|
|
|
return method === 'amber' ? 'amber' : undefined;
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
return undefined;
|
2025-04-02 23:40:54 -04:00
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
console.error('[AuthService] Error getting current auth method:', error);
|
|
|
|
return undefined;
|
2025-04-02 23:40:54 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|