Still don't have dm working because I can't decrypt at primal.
This commit is contained in:
315
lite/build.js
Normal file
315
lite/build.js
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Simple script to create NOSTR_LOGIN_LITE bundle
|
||||
* For the new two-file architecture:
|
||||
* 1. nostr.bundle.js (official nostr-tools bundle - static file)
|
||||
* 2. nip46-extension.js (NIP-46 extension - static file)
|
||||
* 3. nostr-lite.js (NOSTR_LOGIN_LITE library - built by this script)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function createNostrLoginLiteBundle() {
|
||||
console.log('🔧 Creating NOSTR_LOGIN_LITE bundle for two-file architecture...');
|
||||
|
||||
const outputPath = path.join(__dirname, 'nostr-lite.js');
|
||||
|
||||
// Remove old bundle
|
||||
try {
|
||||
if (fs.existsSync(outputPath)) {
|
||||
fs.unlinkSync(outputPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No old bundle to remove');
|
||||
}
|
||||
|
||||
// Start with the bundle header
|
||||
let bundle = `/**
|
||||
* NOSTR_LOGIN_LITE - Authentication Library
|
||||
* Two-file architecture:
|
||||
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
||||
* 2. Load nip46-extension.js (extends NostrTools with NIP-46)
|
||||
* 3. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library)
|
||||
* Generated on: ${new Date().toISOString()}
|
||||
*/
|
||||
|
||||
// Verify dependencies are loaded
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!window.NostrTools) {
|
||||
console.error('NOSTR_LOGIN_LITE: nostr.bundle.js must be loaded first');
|
||||
throw new Error('Missing dependency: nostr.bundle.js');
|
||||
}
|
||||
|
||||
if (!window.NostrTools.nip46) {
|
||||
console.error('NOSTR_LOGIN_LITE: nip46-extension.js must be loaded after nostr.bundle.js');
|
||||
throw new Error('Missing dependency: nip46-extension.js');
|
||||
}
|
||||
|
||||
console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓');
|
||||
console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools));
|
||||
console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46);
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// NOSTR_LOGIN_LITE Components
|
||||
// ======================================
|
||||
|
||||
`;
|
||||
|
||||
// Add Modal UI
|
||||
const modalPath = path.join(__dirname, 'ui/modal.js');
|
||||
if (fs.existsSync(modalPath)) {
|
||||
console.log('📄 Adding Modal UI...');
|
||||
|
||||
let modalContent = fs.readFileSync(modalPath, 'utf8');
|
||||
|
||||
// Skip header comments
|
||||
let lines = modalContent.split('\n');
|
||||
let contentStartIndex = 0;
|
||||
|
||||
for (let i = 0; i < Math.min(15, lines.length); i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('/**') || line.startsWith('*') ||
|
||||
line.startsWith('/*') || line.startsWith('//')) {
|
||||
contentStartIndex = i + 1;
|
||||
} else if (line && !line.startsWith('*') && !line.startsWith('//')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentStartIndex > 0) {
|
||||
lines = lines.slice(contentStartIndex);
|
||||
}
|
||||
|
||||
bundle += `// ======================================\n`;
|
||||
bundle += `// Modal UI Component\n`;
|
||||
bundle += `// ======================================\n\n`;
|
||||
bundle += lines.join('\n');
|
||||
bundle += '\n\n';
|
||||
} else {
|
||||
console.warn('⚠️ Modal UI not found: ui/modal.js');
|
||||
}
|
||||
|
||||
// Add main library code
|
||||
console.log('📄 Adding Main Library...');
|
||||
bundle += `
|
||||
// ======================================
|
||||
// Main NOSTR_LOGIN_LITE Library
|
||||
// ======================================
|
||||
|
||||
// Extension Bridge for managing browser extensions
|
||||
class ExtensionBridge {
|
||||
constructor() {
|
||||
this.extensions = new Map();
|
||||
this.primaryExtension = null;
|
||||
this._detectExtensions();
|
||||
}
|
||||
|
||||
_detectExtensions() {
|
||||
// Common extension locations
|
||||
const locations = [
|
||||
{ path: 'window.nostr', name: 'Generic' },
|
||||
{ path: 'window.alby?.nostr', name: 'Alby' },
|
||||
{ path: 'window.nos2x?.nostr', name: 'nos2x' },
|
||||
{ path: 'window.flamingo?.nostr', name: 'Flamingo' },
|
||||
{ path: 'window.getAlby?.nostr', name: 'Alby Legacy' },
|
||||
{ path: 'window.mutiny?.nostr', name: 'Mutiny' }
|
||||
];
|
||||
|
||||
for (const location of locations) {
|
||||
try {
|
||||
const obj = eval(location.path);
|
||||
if (obj && typeof obj.getPublicKey === 'function') {
|
||||
this.extensions.set(location.name, {
|
||||
name: location.name,
|
||||
extension: obj,
|
||||
constructor: obj.constructor?.name || 'Unknown'
|
||||
});
|
||||
|
||||
if (!this.primaryExtension) {
|
||||
this.primaryExtension = this.extensions.get(location.name);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Extension not available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAllExtensions() {
|
||||
return Array.from(this.extensions.values());
|
||||
}
|
||||
|
||||
getExtensionCount() {
|
||||
return this.extensions.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Main NostrLite class
|
||||
class NostrLite {
|
||||
constructor() {
|
||||
this.options = {};
|
||||
this.extensionBridge = new ExtensionBridge();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async init(options = {}) {
|
||||
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
|
||||
|
||||
this.options = {
|
||||
theme: 'light',
|
||||
darkMode: false,
|
||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: true,
|
||||
connect: false,
|
||||
otp: false
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
// Set up window.nostr facade if no extension detected
|
||||
if (this.extensionBridge.getExtensionCount() === 0) {
|
||||
this._setupWindowNostrFacade();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
console.log('NOSTR_LOGIN_LITE: Initialization complete');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
_setupWindowNostrFacade() {
|
||||
if (typeof window !== 'undefined' && !window.nostr) {
|
||||
window.nostr = new WindowNostr(this);
|
||||
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed');
|
||||
}
|
||||
}
|
||||
|
||||
launch(startScreen = 'login') {
|
||||
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
||||
|
||||
if (typeof Modal !== 'undefined') {
|
||||
const modal = new Modal(this.options);
|
||||
modal.open({ startScreen });
|
||||
} else {
|
||||
console.error('NOSTR_LOGIN_LITE: Modal component not available');
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
console.log('NOSTR_LOGIN_LITE: Logout called');
|
||||
|
||||
// Clear stored data
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem('nl_current');
|
||||
}
|
||||
|
||||
// Dispatch logout event
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('nlLogout', {
|
||||
detail: { timestamp: Date.now() }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Window.nostr facade for when no extension is available
|
||||
class WindowNostr {
|
||||
constructor(nostrLite) {
|
||||
this.nostrLite = nostrLite;
|
||||
}
|
||||
|
||||
async getPublicKey() {
|
||||
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
|
||||
}
|
||||
|
||||
async signEvent(event) {
|
||||
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
|
||||
}
|
||||
|
||||
async getRelays() {
|
||||
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
|
||||
}
|
||||
|
||||
get nip04() {
|
||||
return {
|
||||
async encrypt(pubkey, plaintext) {
|
||||
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
|
||||
},
|
||||
async decrypt(pubkey, ciphertext) {
|
||||
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get nip44() {
|
||||
return {
|
||||
async encrypt(pubkey, plaintext) {
|
||||
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
|
||||
},
|
||||
async decrypt(pubkey, ciphertext) {
|
||||
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and export
|
||||
if (typeof window !== 'undefined') {
|
||||
const nostrLite = new NostrLite();
|
||||
|
||||
// Export main API
|
||||
window.NOSTR_LOGIN_LITE = {
|
||||
init: (options) => nostrLite.init(options),
|
||||
launch: (startScreen) => nostrLite.launch(startScreen),
|
||||
logout: () => nostrLite.logout(),
|
||||
|
||||
// Expose for debugging
|
||||
_extensionBridge: nostrLite.extensionBridge,
|
||||
_instance: nostrLite
|
||||
};
|
||||
|
||||
console.log('NOSTR_LOGIN_LITE: Library loaded and ready');
|
||||
console.log('NOSTR_LOGIN_LITE: Use window.NOSTR_LOGIN_LITE.init(options) to initialize');
|
||||
console.log('NOSTR_LOGIN_LITE: Detected', nostrLite.extensionBridge.getExtensionCount(), 'browser extensions');
|
||||
} else {
|
||||
// Node.js environment
|
||||
module.exports = { NostrLite };
|
||||
}
|
||||
`;
|
||||
|
||||
// Write the complete bundle
|
||||
fs.writeFileSync(outputPath, bundle, 'utf8');
|
||||
|
||||
const sizeKB = (bundle.length / 1024).toFixed(2);
|
||||
console.log(`\n✅ nostr-lite.js bundle created: ${outputPath}`);
|
||||
console.log(`📏 Bundle size: ${sizeKB} KB`);
|
||||
console.log(`📄 Total lines: ${bundle.split('\n').length}`);
|
||||
|
||||
// Check what's included
|
||||
const hasModal = bundle.includes('class Modal');
|
||||
const hasNostrLite = bundle.includes('NOSTR_LOGIN_LITE');
|
||||
|
||||
console.log('\n📋 Bundle contents:');
|
||||
console.log(` Modal UI: ${hasModal ? '✅ Included' : '❌ Missing'}`);
|
||||
console.log(` NOSTR_LOGIN_LITE: ${hasNostrLite ? '✅ Included' : '❌ Missing'}`);
|
||||
console.log(` Extension Bridge: ✅ Included`);
|
||||
console.log(` Window.nostr facade: ✅ Included`);
|
||||
|
||||
console.log('\n📋 Two-file architecture:');
|
||||
console.log(' 1. nostr.bundle.js (official nostr-tools - 220KB)');
|
||||
console.log(' 2. nip46-extension.js (NIP-46 support - ~15KB)');
|
||||
console.log(` 3. nostr-lite.js (NOSTR_LOGIN_LITE - ${sizeKB}KB)`);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (typeof require !== 'undefined' && require.main === module) {
|
||||
createNostrLoginLiteBundle();
|
||||
}
|
||||
|
||||
module.exports = { createNostrLoginLiteBundle };
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Clean bundler for NOSTR_LOGIN_LITE
|
||||
* Removes problematic files and recreates bundle
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function createCleanBundle() {
|
||||
// First, remove the old bundle if it exists
|
||||
const outputPath = path.join(__dirname, 'nostr-login-lite.bundle.js');
|
||||
try {
|
||||
if (fs.existsSync(outputPath)) {
|
||||
fs.unlinkSync(outputPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No old bundle to remove');
|
||||
}
|
||||
|
||||
const mainFile = path.join(__dirname, 'nostr-login-lite.js');
|
||||
const nip46File = path.join(__dirname, 'core/nip46-client.js');
|
||||
const modalFile = path.join(__dirname, 'ui/modal.js');
|
||||
|
||||
// Start with a clean header
|
||||
let bundle = `/**
|
||||
* NOSTR_LOGIN_LITE
|
||||
* Single-file Nostr authentication library
|
||||
* Generated on: ${new Date().toISOString()}
|
||||
*/
|
||||
|
||||
`;
|
||||
|
||||
// Add section markers and combine files
|
||||
const files = [
|
||||
{ path: modalFile, name: 'modal.js' },
|
||||
{ path: nip46File, name: 'nip46-client.js' },
|
||||
{ path: mainFile, name: 'nostr-login-lite.js' }
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
if (fs.existsSync(file.path)) {
|
||||
const content = fs.readFileSync(file.path, 'utf8');
|
||||
|
||||
bundle += `\n// ======================================\n`;
|
||||
bundle += `// ${file.name}\n`;
|
||||
bundle += `// ======================================\n\n`;
|
||||
|
||||
// Clean the content by removing initial header comments
|
||||
let lines = content.split('\n');
|
||||
let contentStartIndex = 0;
|
||||
|
||||
// Skip the first 10 lines if they contain file headers
|
||||
for (let i = 0; i < Math.min(10, lines.length); i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('/**') || line.startsWith('*') || line.startsWith('/*') || line.startsWith('//') ||
|
||||
line.includes('Copyright') || line.includes('@license') || line.includes('Licensed') || line.includes('©')) {
|
||||
contentStartIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentStartIndex > 0) {
|
||||
lines = lines.slice(contentStartIndex);
|
||||
}
|
||||
|
||||
bundle += lines.join('\n');
|
||||
bundle += '\n\n';
|
||||
|
||||
console.log(`Added ${file.name}`);
|
||||
} else {
|
||||
console.warn(`File not found: ${file.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the bundled file
|
||||
fs.writeFileSync(outputPath, bundle, 'utf8');
|
||||
|
||||
const sizeKB = (bundle.length / 1024).toFixed(2);
|
||||
console.log(`\n✅ Clean bundle created: ${outputPath}`);
|
||||
console.log(`📏 Bundle size: ${sizeKB} KB`);
|
||||
console.log(`📄 Total lines: ${bundle.split('\n').length}`);
|
||||
|
||||
// Verify the bundle starts correctly
|
||||
const firstLines = bundle.split('\n').slice(0, 20).join('\n');
|
||||
console.log('\n📋 First 20 lines:');
|
||||
console.log(firstLines);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { createCleanBundle };
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (typeof require !== 'undefined' && require.main === module) {
|
||||
createCleanBundle().catch(console.error);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Simple bundler for NOSTR_LOGIN_LITE
|
||||
* Combines all files into a single distributable script
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function bundleLite() {
|
||||
const mainFile = path.join(__dirname, 'nostr-login-lite.js');
|
||||
const nip46File = path.join(__dirname, 'core/nip46-client.js');
|
||||
const modalFile = path.join(__dirname, 'ui/modal.js');
|
||||
|
||||
let bundle = `/**
|
||||
* NOSTR_LOGIN_LITE
|
||||
* Single-file Nostr authentication library
|
||||
* Generated on: ${new Date().toISOString()}
|
||||
*/
|
||||
|
||||
// ======================================
|
||||
// Core Classes and Components
|
||||
// ======================================
|
||||
`;
|
||||
|
||||
// Read and combine files
|
||||
const files = [modalFile, nip46File, mainFile];
|
||||
|
||||
for (const file of files) {
|
||||
if (fs.existsSync(file)) {
|
||||
let content = fs.readFileSync(file, 'utf8');
|
||||
|
||||
// Skip the initial comment and license if present
|
||||
let lines = content.split('\n');
|
||||
|
||||
// Find and skip complete JSDoc blocks at the beginning
|
||||
let skipUntil = 0;
|
||||
let inJSDocBlock = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('/**')) {
|
||||
inJSDocBlock = true;
|
||||
skipUntil = i;
|
||||
} else if (inJSDocBlock && line.startsWith('*/')) {
|
||||
skipUntil = i;
|
||||
break;
|
||||
} else if (i < 10 && (line.startsWith('const') || line.startsWith('class') || line.startsWith('function'))) {
|
||||
// Hit actual code before finding end of JSDoc block
|
||||
inJSDocBlock = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (inJSDocBlock) {
|
||||
lines = lines.slice(skipUntil + 1); // Skip the entire JSDoc block
|
||||
} else {
|
||||
// Fallback to old filtering (skip comment-like lines in first 10)
|
||||
lines = lines.filter((line, index) => {
|
||||
return index >= 10 || !line.trim().startsWith('*') && !line.trim().startsWith('//');
|
||||
});
|
||||
}
|
||||
|
||||
bundle += '\n// ======================================\n';
|
||||
bundle += `// ${path.basename(file)}\n`;
|
||||
bundle += '// ======================================\n\n';
|
||||
bundle += lines.join('\n');
|
||||
bundle += '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Write the bundled file
|
||||
const outputPath = path.join(__dirname, 'nostr-login-lite.bundle.js');
|
||||
fs.writeFileSync(outputPath, bundle);
|
||||
|
||||
console.log('Bundle created:', outputPath);
|
||||
console.log('Bundle size:', (bundle.length / 1024).toFixed(2), 'KB');
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { bundleLite };
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (typeof require !== 'undefined' && require.main === module) {
|
||||
bundleLite().catch(console.error);
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
/**
|
||||
* NOSTR NIP-46 Client Implementation
|
||||
* Minimal RPC over NostrTools.SimplePool for NOSTR_LOGIN_LITE
|
||||
*/
|
||||
|
||||
class NIP46Client {
|
||||
constructor() {
|
||||
this.pool = null;
|
||||
this.localSk = null;
|
||||
this.localPk = null;
|
||||
this.remotePk = null;
|
||||
this.relays = [];
|
||||
this.sub = null;
|
||||
this.pendingRequests = {};
|
||||
this.useNip44 = false;
|
||||
this.iframeOrigin = null;
|
||||
this.iframePort = null;
|
||||
}
|
||||
|
||||
init(localSk, remotePk, relays, iframeOrigin) {
|
||||
// Create SimplePool
|
||||
this.pool = new window.NostrTools.SimplePool();
|
||||
|
||||
// Setup keys
|
||||
this.localSk = localSk;
|
||||
if (this.localSk) {
|
||||
this.localPk = window.NostrTools.getPublicKey(this.localSk);
|
||||
}
|
||||
|
||||
this.remotePk = remotePk;
|
||||
this.relays = [...relays];
|
||||
|
||||
// Store iframe origin for future use
|
||||
this.iframeOrigin = iframeOrigin;
|
||||
|
||||
console.log('NIP46Client initialized for', this.remotePk ? 'remote signer' : 'listening mode');
|
||||
}
|
||||
|
||||
setUseNip44(use) {
|
||||
this.useNip44 = use;
|
||||
}
|
||||
|
||||
subscribeReplies() {
|
||||
if (!this.pool || !this.localPk) return;
|
||||
|
||||
// Subscribe to replies to our pubkey on kind 24133 (NIP-46 methods)
|
||||
this.sub = this.pool.sub(this.relays, [{
|
||||
kinds: [24133],
|
||||
'#p': [this.localPk]
|
||||
}]);
|
||||
|
||||
this.sub.on('event', (event) => this.onEvent(event));
|
||||
this.sub.on('eose', () => {
|
||||
console.log('NIP-46 subscription caught up');
|
||||
});
|
||||
|
||||
console.log('Subscribed to NIP-46 replies on relays:', this.relays);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
if (this.sub) {
|
||||
this.sub.unsub();
|
||||
this.sub = null;
|
||||
}
|
||||
}
|
||||
|
||||
async onEvent(event) {
|
||||
console.log('NIP-46 event received:', event);
|
||||
|
||||
try {
|
||||
const parsed = await this.parseEvent(event);
|
||||
if (parsed) {
|
||||
if (parsed.id && this.pendingRequests[parsed.id]) {
|
||||
// Handle response
|
||||
const handler = this.pendingRequests[parsed.id];
|
||||
delete this.pendingRequests[parsed.id];
|
||||
|
||||
if (parsed.result !== undefined) {
|
||||
handler.resolve(parsed.result);
|
||||
} else if (parsed.error) {
|
||||
handler.reject(new Error(parsed.error));
|
||||
} else {
|
||||
handler.reject(new Error('Invalid response format'));
|
||||
}
|
||||
} else if (parsed.method === 'auth_url') {
|
||||
// Handle auth_url emissions (deduplication required)
|
||||
this.emitAuthUrlIfNeeded(parsed.params[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing NIP-46 event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
emitAuthUrlIfNeeded(url) {
|
||||
// Deduplicate auth_url emissions - only emit if not recently shown
|
||||
const lastUrl = sessionStorage.getItem('nl-last-auth-url');
|
||||
if (lastUrl === url) {
|
||||
console.log('Auth URL already shown, skipping duplicate:', url);
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('nl-last-auth-url', url);
|
||||
console.log('New auth URL:', url);
|
||||
|
||||
// Emit event for UI
|
||||
window.dispatchEvent(new CustomEvent('nlAuthUrl', { detail: { url } }));
|
||||
}
|
||||
|
||||
async parseEvent(event) {
|
||||
try {
|
||||
let content = event.content;
|
||||
|
||||
// Determine encryption method based on content structure
|
||||
if (content.length > 44) {
|
||||
// Likely NIP-44 (encrypted)
|
||||
if (this.localSk && event.pubkey) {
|
||||
try {
|
||||
content = window.NostrTools.nip44?.decrypt(this.localSk, event.pubkey, content);
|
||||
} catch (e) {
|
||||
console.warn('NIP-44 decryption failed, trying NIP-04...');
|
||||
content = await window.NostrTools.nip04.decrypt(this.localSk, event.pubkey, content);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Likely NIP-04
|
||||
if (this.localSk && event.pubkey) {
|
||||
content = await window.NostrTools.nip04.decrypt(this.localSk, event.pubkey, content);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = JSON.parse(content);
|
||||
console.log('Decrypted NIP-46 payload:', payload);
|
||||
|
||||
return {
|
||||
id: payload.id,
|
||||
method: payload.method,
|
||||
params: payload.params,
|
||||
result: payload.result,
|
||||
error: payload.error,
|
||||
event: event
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error('Failed to parse event:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async listen(nostrConnectSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.localPk) {
|
||||
reject(new Error('No local pubkey available for listening'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to unsolicited events to our pubkey
|
||||
let foundSecretOrAck = false;
|
||||
|
||||
const listenSub = this.pool.sub(this.relays, [{
|
||||
kinds: [24133],
|
||||
'#p': [this.localPk]
|
||||
}]);
|
||||
|
||||
listenSub.on('event', async (event) => {
|
||||
try {
|
||||
const parsed = await this.parseEvent(event);
|
||||
if (parsed && parsed.method === 'connect') {
|
||||
// Accept if it's an ack or matches our secret
|
||||
const [userPubkey, token] = parsed.params || [];
|
||||
|
||||
if (token === '' && parsed.result === 'ack') {
|
||||
// Ack received
|
||||
foundSecretOrAck = true;
|
||||
listenSub.unsub();
|
||||
resolve(event.pubkey);
|
||||
} else if (token === nostrConnectSecret) {
|
||||
// Secret match
|
||||
foundSecretOrAck = true;
|
||||
listenSub.unsub();
|
||||
resolve(event.pubkey);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in listen mode:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout after 5 minutes
|
||||
setTimeout(() => {
|
||||
if (!foundSecretOrAck) {
|
||||
listenSub.unsub();
|
||||
reject(new Error('Listen timeout - no signer connected'));
|
||||
}
|
||||
}, 300000);
|
||||
});
|
||||
}
|
||||
|
||||
async connect(token, perms) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const result = await this.sendRequest(
|
||||
this.remotePk,
|
||||
'connect',
|
||||
[this.localPk, token || '', perms || ''],
|
||||
24133,
|
||||
(response) => {
|
||||
if (response === 'ack') {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('Connection not acknowledged'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Set 30 second timeout
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 30000);
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async initUserPubkey(hint) {
|
||||
if (hint) {
|
||||
this.remotePk = hint;
|
||||
return hint;
|
||||
}
|
||||
|
||||
if (!this.remotePk) {
|
||||
// Request get_public_key
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const pubkey = await this.sendRequest(
|
||||
this.remotePk,
|
||||
'get_public_key',
|
||||
[],
|
||||
24133
|
||||
);
|
||||
this.remotePk = pubkey;
|
||||
resolve(pubkey);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.remotePk;
|
||||
}
|
||||
|
||||
async sendRequest(remotePubkey, method, params, kind = 24133, cb) {
|
||||
if (!this.pool || !this.localSk || !this.localPk) {
|
||||
throw new Error('NIP46Client not properly initialized');
|
||||
}
|
||||
|
||||
if (!remotePubkey) {
|
||||
throw new Error('No remote pubkey specified');
|
||||
}
|
||||
|
||||
const id = this._generateId();
|
||||
|
||||
// Create request event
|
||||
const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
|
||||
|
||||
console.log('Sending NIP-46 request:', { id, method, params });
|
||||
|
||||
// Publish to relays
|
||||
const pubs = await this.pool.publish(this.relays, event);
|
||||
console.log('Published to relays, waiting for response...');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Set timeout
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('NIP-46 request timeout for id:', id);
|
||||
delete this.pendingRequests[id];
|
||||
reject(new Error(`Request timeout for ${method}`));
|
||||
}, 60000); // 1 minute timeout
|
||||
|
||||
// Store handler
|
||||
this.pendingRequests[id] = {
|
||||
resolve: (result) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(result);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// If callback provided, override resolve
|
||||
if (cb) {
|
||||
const originalResolve = this.pendingRequests[id].resolve;
|
||||
this.pendingRequests[id].resolve = (result) => {
|
||||
cb(result);
|
||||
originalResolve(result);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createRequestEvent(id, remotePubkey, method, params, kind = 24133) {
|
||||
let content = JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params
|
||||
});
|
||||
|
||||
// Choose encryption method
|
||||
let encrypted = content;
|
||||
if (method !== 'create_account') {
|
||||
// Use NIP-44 for non-account creation methods if available
|
||||
if (this.useNip44 && window.NostrTools.nip44) {
|
||||
encrypted = window.NostrTools.nip44.encrypt(this.localSk, remotePubkey, content);
|
||||
} else {
|
||||
// Fallback to NIP-04
|
||||
encrypted = await window.NostrTools.nip04.encrypt(this.localSk, remotePubkey, content);
|
||||
}
|
||||
}
|
||||
|
||||
// Create event structure
|
||||
const event = {
|
||||
kind: kind,
|
||||
content: encrypted,
|
||||
tags: [
|
||||
['p', remotePubkey]
|
||||
],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: this.localPk,
|
||||
id: '', // Will be set by finalizeEvent
|
||||
sig: '' // Will be set by finalizeEvent
|
||||
};
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = window.NostrTools.finalizeEvent(event, this.localSk);
|
||||
|
||||
return signedEvent;
|
||||
}
|
||||
|
||||
_generateId() {
|
||||
return 'nl-' + Date.now() + '-' + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
setWorkerIframePort(port) {
|
||||
this.iframePort = port;
|
||||
|
||||
// Set up postMessage routing if needed
|
||||
if (this.iframePort && this.iframeOrigin) {
|
||||
this.iframePort.onmessage = (event) => {
|
||||
if (event.origin !== this.iframeOrigin) {
|
||||
console.warn('Ignoring message from unknown origin:', event.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Received iframe message:', event.data);
|
||||
// Handle iframe messages
|
||||
};
|
||||
|
||||
// Send keepalive
|
||||
setInterval(() => {
|
||||
if (this.iframePort) {
|
||||
try {
|
||||
this.iframePort.postMessage({ type: 'ping' });
|
||||
} catch (e) {
|
||||
console.warn('Iframe port closed');
|
||||
this.iframePort = null;
|
||||
}
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
}
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.unsubscribe();
|
||||
|
||||
if (this.iframePort) {
|
||||
try {
|
||||
this.iframePort.close();
|
||||
} catch (e) {
|
||||
console.warn('Error closing iframe port:', e);
|
||||
}
|
||||
this.iframePort = null;
|
||||
}
|
||||
|
||||
if (this.pool) {
|
||||
this.pool.close(this.relays);
|
||||
this.pool = null;
|
||||
}
|
||||
|
||||
// Clear all pending requests
|
||||
for (const id in this.pendingRequests) {
|
||||
this.pendingRequests[id].reject(new Error('Client teardown'));
|
||||
}
|
||||
this.pendingRequests = {};
|
||||
}
|
||||
}
|
||||
2379
lite/nostr-lite.js
Normal file
2379
lite/nostr-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -165,8 +165,8 @@ class Modal {
|
||||
});
|
||||
}
|
||||
|
||||
// Nostr Connect option
|
||||
if (this.options?.methods?.connect !== false) {
|
||||
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
|
||||
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
||||
options.push({
|
||||
type: 'connect',
|
||||
title: 'Nostr Connect',
|
||||
@@ -186,7 +186,7 @@ class Modal {
|
||||
}
|
||||
|
||||
// OTP/DM option
|
||||
if (this.options?.methods?.otp !== false && this.options?.otp) {
|
||||
if (this.options?.methods?.otp !== false) {
|
||||
options.push({
|
||||
type: 'otp',
|
||||
title: 'DM/OTP',
|
||||
@@ -858,11 +858,7 @@ class Modal {
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Pre-fill with our bunker config if available
|
||||
if (window.NIP46_BUNKER_CONFIG) {
|
||||
pubkeyInput.value = window.NIP46_BUNKER_CONFIG.remoteSigner.pubkey;
|
||||
urlInput.value = window.NIP46_BUNKER_CONFIG.remoteSigner.url;
|
||||
}
|
||||
// Users will enter the bunker URL manually from their bunker setup
|
||||
|
||||
const connectButton = document.createElement('button');
|
||||
connectButton.textContent = 'Connect to Bunker';
|
||||
@@ -956,9 +952,9 @@ class Modal {
|
||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||
console.log('Generated local client keypair for NIP-46 session');
|
||||
|
||||
// Use nostr-tools BunkerSigner.fromBunker() for bunker:// connections
|
||||
// Use nostr-tools BunkerSigner constructor
|
||||
console.log('Creating nip46 BunkerSigner...');
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
const signer = new window.NostrTools.nip46.BunkerSigner(localSecretKey, bunkerPointer, {
|
||||
onauth: (url) => {
|
||||
console.log('Received auth URL from bunker:', url);
|
||||
// Open auth URL in popup or redirect
|
||||
@@ -968,12 +964,8 @@ class Modal {
|
||||
|
||||
console.log('NIP-46 BunkerSigner created successfully');
|
||||
|
||||
// Attempt initial ping to verify connection
|
||||
console.log('Testing bunker connection with ping...');
|
||||
await signer.ping();
|
||||
console.log('NIP-46 ping successful - bunker is reachable');
|
||||
|
||||
// Try to connect (this may trigger auth flow)
|
||||
// Skip ping test - NIP-46 works through relays, not direct connection
|
||||
// Try to connect directly (this may trigger auth flow)
|
||||
console.log('Attempting NIP-46 connect...');
|
||||
await signer.connect();
|
||||
console.log('NIP-46 connect successful');
|
||||
|
||||
Reference in New Issue
Block a user