extensions and nsec login working
This commit is contained in:
260
lite/README.md
Normal file
260
lite/README.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# NOSTR_LOGIN_LITE
|
||||
|
||||
A minimal, dependency-light replacement for the current auth/UI stack that preserves all login methods and window.nostr surface.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Single file distributable (`nostr-login-lite.bundle.js`)
|
||||
- ✅ All major login methods: Extension, Local key, Read-only
|
||||
- ✅ Compatible window.nostr facade
|
||||
- ✅ NIP-04 and NIP-44 encryption (local)
|
||||
- ✅ Minimal vanilla JavaScript modal
|
||||
- ✅ No bulky frameworks (no Stencil/Tailwind)
|
||||
- ✅ Just ~50KB gzipped (minus nostr-tools)
|
||||
|
||||
## Installation
|
||||
|
||||
```html
|
||||
<!-- 1. Load nostr-tools (local bundle) -->
|
||||
<script src="./lite/nostr.bundle.js"></script>
|
||||
|
||||
<!-- 2. Load NOSTR_LOGIN_LITE -->
|
||||
<script src="./lite/nostr-login-lite.bundle.js"></script>
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
// Initialize with default options
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
theme: 'light',
|
||||
relays: ['wss://relay.damus.io'],
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: true
|
||||
}
|
||||
});
|
||||
|
||||
// Launch login modal
|
||||
window.NOSTR_LOGIN_LITE.launch();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Initialization
|
||||
```javascript
|
||||
// Initialize the library
|
||||
await window.NOSTR_LOGIN_LITE.init(options)
|
||||
|
||||
// Options
|
||||
{
|
||||
theme: 'light' | 'dark',
|
||||
darkMode: boolean,
|
||||
relays: string[], // Default relays for NIP-46
|
||||
methods: {
|
||||
connect: boolean, // NIP-46 providers (coming)
|
||||
extension: boolean, // Browser extensions
|
||||
local: boolean, // Local key generation
|
||||
readonly: boolean, // Read-only mode
|
||||
otp: boolean // OTP/DM (coming)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### User Interface
|
||||
```javascript
|
||||
// Launch authentication modal
|
||||
window.NOSTR_LOGIN_LITE.launch(startScreen);
|
||||
|
||||
// Available screens
|
||||
'login' // Auth selection
|
||||
'signup' // Account creation
|
||||
'switch' // Account switching
|
||||
|
||||
// Logout current user
|
||||
window.NOSTR_LOGIN_LITE.logout();
|
||||
|
||||
// Set theme
|
||||
window.NOSTR_LOGIN_LITE.setDarkMode(dark: boolean);
|
||||
```
|
||||
|
||||
### Programmatic Auth
|
||||
```javascript
|
||||
// Set authentication manually
|
||||
window.NOSTR_LOGIN_LITE.setAuth({
|
||||
type: 'login' | 'signup' | 'logout',
|
||||
method: 'extension' | 'local' | 'readonly' | 'connect' | 'otp',
|
||||
pubkey: string, // Public key
|
||||
secret: string // Private key (optional)
|
||||
});
|
||||
|
||||
// Cancel ongoing auth flow
|
||||
window.NOSTR_LOGIN_LITE.cancelNeedAuth();
|
||||
```
|
||||
|
||||
### window.nostr Compatibility
|
||||
|
||||
All standard window.nostr methods work transparently:
|
||||
|
||||
```javascript
|
||||
// Get current user's public key
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
|
||||
// Sign an event
|
||||
const signed = await window.nostr.signEvent({
|
||||
kind: 1,
|
||||
content: 'Hello Nostr!',
|
||||
tags: [],
|
||||
created_at: Date.now()
|
||||
});
|
||||
|
||||
// NIP-04 encrypt/decrypt
|
||||
const encrypted = await window.nostr.nip04.encrypt(recipientPubkey, 'secret');
|
||||
const decrypted = await window.nostr.nip04.decrypt(senderPubkey, encrypted);
|
||||
|
||||
// NIP-44 encrypt/decrypt (if available in nostr-tools)
|
||||
const encrypted = await window.nostr.nip44.encrypt(recipientPubkey, 'secret');
|
||||
const decrypted = await window.nostr.nip44.decrypt(senderPubkey, encrypted);
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
Listen for authentication events:
|
||||
|
||||
```javascript
|
||||
// Auth state changes
|
||||
window.addEventListener('nlAuth', (event) => {
|
||||
console.log(event.detail);
|
||||
// { type: 'login', pubkey: '...', method: 'local' }
|
||||
});
|
||||
|
||||
// Dark mode changes
|
||||
window.addEventListener('nlDarkMode', (event) => {
|
||||
document.body.classList.toggle('dark', event.detail.dark);
|
||||
});
|
||||
|
||||
// Auth URLs (for NIP-46)
|
||||
window.addEventListener('nlAuthUrl', (event) => {
|
||||
console.log('Auth URL:', event.detail.url);
|
||||
});
|
||||
|
||||
// Logout events
|
||||
window.addEventListener('nlLogout', () => {
|
||||
console.log('User logged out');
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
NOSTR_LOGIN_LITE/
|
||||
├── nostr-login-lite.bundle.js # Single distributable
|
||||
├── core/
|
||||
│ ├── nip46-client.js # NIP-46 transport over websockets
|
||||
│ └── {other components} # Future components
|
||||
├── ui/
|
||||
│ └── modal.js # Vanilla JS modal
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
- **Modal**: Lightweight modal using vanilla CSS
|
||||
- **Store**: localStorage helpers for accounts/relays
|
||||
- **LocalSigner**: Wraps nostr-tools for local signing
|
||||
- **ExtensionBridge**: Detects and bridges browser extensions
|
||||
- **NIP46Client**: Handles remote signing (NIP-46)
|
||||
- **Relays**: Default relay management
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Modern browsers with ES2018+ support
|
||||
- WebWorkers (for NIP-46)
|
||||
- localStorage (for account storage)
|
||||
|
||||
## Dependencies
|
||||
|
||||
This project uses a local copy of nostr-tools instead of loading from CDN:
|
||||
|
||||
- **nostr.bundle.js**: Local nostr-tools bundle (~200KB)
|
||||
- **nostr-login-lite.bundle.js**: This library (~50KB gzipped)
|
||||
|
||||
## Size Comparison
|
||||
|
||||
```
|
||||
nostr-tools ~200KB (served locally)
|
||||
NOSTR_LOGIN_LITE ~50KB (gzipped)
|
||||
Total: ~250KB
|
||||
|
||||
vs.
|
||||
|
||||
Full nostr-login: ~2.5MB+ framework dependencies
|
||||
```
|
||||
|
||||
## Future Roadmap ⚠️
|
||||
|
||||
The following features are planned but not yet implemented:
|
||||
|
||||
- **NIP-46 Connect**: External signer integration
|
||||
- **OTP/DM**: Server-side OTP delivery
|
||||
- **Connect Wallet**: Wallet integration
|
||||
- **Advanced UI**: Multi-screen flows
|
||||
- **NIP-05 Profiles**: User discovery
|
||||
- **Backup/Restore**: Key backup functionality
|
||||
|
||||
## Development
|
||||
|
||||
To work on the source files:
|
||||
|
||||
```bash
|
||||
# Edit individual components
|
||||
lite/core/nip46-client.js
|
||||
lite/ui/modal.js
|
||||
lite/nostr-login-lite.js
|
||||
|
||||
# Run bundler to create distribution
|
||||
node lite/bundler.js
|
||||
|
||||
# Start dev server (from project root)
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Open test page
|
||||
open http://localhost:8000/examples/simple-demo.html
|
||||
```
|
||||
|
||||
### Local Bundle Setup
|
||||
|
||||
The project uses a local copy of nostr-tools:
|
||||
- `lite/nostr.bundle.js` - Local nostr-tools bundle (serves from this location)
|
||||
- `nostr-tools/` - Reference directory (excluded from git, not used in runtime)
|
||||
- Examples load from: `../lite/nostr.bundle.js`
|
||||
|
||||
## Examples
|
||||
|
||||
See `examples/` directory:
|
||||
- `simple-demo.html` - Basic functionality demo
|
||||
- `full-test.html` - Comprehensive test suite
|
||||
- `test-lite.html` - Minimal functionality test
|
||||
|
||||
## Migration from Full nostr-login
|
||||
|
||||
The lite version provides the same API surface, but is much smaller:
|
||||
|
||||
```javascript
|
||||
// Before (full library)
|
||||
import { init, launch, logout } from 'nostr-login';
|
||||
|
||||
// After (lite version)
|
||||
await window.NOSTR_LOGIN_LITE.init();
|
||||
window.NOSTR_LOGIN_LITE.launch();
|
||||
window.NOSTR_LOGIN_LITE.logout();
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Same license as the original nostr-login project.
|
||||
|
||||
## Contributing
|
||||
|
||||
This is meant to be lightweight and focused. Contributions should maintain the minimal footprint while extending functionality.
|
||||
97
lite/bundler-clean.js
Normal file
97
lite/bundler-clean.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
86
lite/bundler.js
Normal file
86
lite/bundler.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
398
lite/core/nip46-client.js
Normal file
398
lite/core/nip46-client.js
Normal file
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* 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 = {};
|
||||
}
|
||||
}
|
||||
2339
lite/nostr-login-lite.bundle.js
Normal file
2339
lite/nostr-login-lite.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
918
lite/nostr-login-lite.js
Normal file
918
lite/nostr-login-lite.js
Normal file
@@ -0,0 +1,918 @@
|
||||
/**
|
||||
* NOSTR_LOGIN_LITE
|
||||
* A minimal, dependency-light replacement for the current auth/UI stack
|
||||
* Preserves all login methods and window.nostr surface
|
||||
*/
|
||||
|
||||
// Import NIP-46 client
|
||||
if (typeof NIP46Client === 'undefined') {
|
||||
// Load NIP46Client if not already available (for non-bundled version)
|
||||
const script = document.createElement('script');
|
||||
script.src = './core/nip46-client.js';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
// Global state
|
||||
const LiteState = {
|
||||
initialized: false,
|
||||
windowNostr: null,
|
||||
options: null,
|
||||
auth: null,
|
||||
modal: null,
|
||||
bus: null,
|
||||
pool: null,
|
||||
nip44Codec: null,
|
||||
extensionBridge: null,
|
||||
nip46Client: null
|
||||
};
|
||||
|
||||
// Dependencies verification
|
||||
class Deps {
|
||||
static ensureNostrToolsLoaded() {
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error('NOSTR_LOGIN_LITE must run in browser environment');
|
||||
}
|
||||
|
||||
if (!window.NostrTools) {
|
||||
throw new Error(
|
||||
'window.NostrTools is required but not loaded. ' +
|
||||
'Please include: <script src="./lite/nostr.bundle.js"></script>'
|
||||
);
|
||||
}
|
||||
|
||||
// Verify required APIs
|
||||
const required = ['SimplePool', 'getPublicKey', 'finalizeEvent', 'nip04'];
|
||||
for (const api of required) {
|
||||
if (!window.NostrTools[api]) {
|
||||
throw new Error(`window.NostrTools.${api} is required but missing`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for key generation function (might be generateSecretKey or generatePrivateKey)
|
||||
if (!window.NostrTools.generateSecretKey && !window.NostrTools.generatePrivateKey) {
|
||||
throw new Error('window.NostrTools must have either generateSecretKey or generatePrivateKey');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Event Bus for internal communication
|
||||
class Bus {
|
||||
constructor() {
|
||||
this.handlers = {};
|
||||
}
|
||||
|
||||
on(event, handler) {
|
||||
if (!this.handlers[event]) {
|
||||
this.handlers[event] = [];
|
||||
}
|
||||
this.handlers[event].push(handler);
|
||||
}
|
||||
|
||||
off(event, handler) {
|
||||
if (!this.handlers[event]) return;
|
||||
this.handlers[event] = this.handlers[event].filter(h => h !== handler);
|
||||
}
|
||||
|
||||
emit(event, payload) {
|
||||
if (!this.handlers[event]) return;
|
||||
this.handlers[event].forEach(handler => {
|
||||
try {
|
||||
handler(payload);
|
||||
} catch (e) {
|
||||
console.error(`Error in event handler for ${event}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Storage helpers
|
||||
class Store {
|
||||
static addAccount(info) {
|
||||
const accounts = this.getAccounts();
|
||||
// Remove existing account with same pubkey if present
|
||||
const filtered = accounts.filter(acc => acc.pubkey !== info.pubkey);
|
||||
filtered.push(info);
|
||||
localStorage.setItem('nl_accounts', JSON.stringify(filtered));
|
||||
}
|
||||
|
||||
static removeCurrentAccount() {
|
||||
const current = this.getCurrent();
|
||||
if (current && current.pubkey) {
|
||||
const accounts = this.getAccounts();
|
||||
const filtered = accounts.filter(acc => acc.pubkey !== current.pubkey);
|
||||
localStorage.setItem('nl_accounts', JSON.stringify(filtered));
|
||||
localStorage.removeItem('nl_current');
|
||||
}
|
||||
}
|
||||
|
||||
static getCurrent() {
|
||||
try {
|
||||
const stored = localStorage.getItem('nl_current');
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
} catch (e) {
|
||||
console.error('Error parsing current account:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static setCurrent(info) {
|
||||
localStorage.setItem('nl_current', JSON.stringify(info));
|
||||
}
|
||||
|
||||
static getAccounts() {
|
||||
try {
|
||||
const stored = localStorage.getItem('nl_accounts');
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (e) {
|
||||
console.error('Error parsing accounts:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static getRecents() {
|
||||
// Return last 5 used accounts in reverse chronological order
|
||||
const accounts = this.getAccounts().slice(-5).reverse();
|
||||
return accounts;
|
||||
}
|
||||
|
||||
static setItem(key, value) {
|
||||
localStorage.setItem(`nl-${key}`, value);
|
||||
}
|
||||
|
||||
static getItem(key) {
|
||||
return localStorage.getItem(`nl-${key}`);
|
||||
}
|
||||
|
||||
static async getIcon() {
|
||||
// Simple default icon - could be extended to fetch from profile
|
||||
return '🔑';
|
||||
}
|
||||
}
|
||||
|
||||
// Relay configuration helpers
|
||||
class Relays {
|
||||
static getDefaultRelays(options) {
|
||||
if (options?.relays) {
|
||||
return this.normalize(options.relays);
|
||||
}
|
||||
|
||||
// Default relays for fallbacks
|
||||
return [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.snort.social',
|
||||
'wss://nos.lol'
|
||||
];
|
||||
}
|
||||
|
||||
static normalize(relays) {
|
||||
return relays.map(relay => {
|
||||
// Ensure wss:// prefix
|
||||
if (relay.startsWith('ws://')) {
|
||||
return relay.replace('ws://', 'wss://');
|
||||
} else if (!relay.startsWith('wss://')) {
|
||||
return `wss://${relay}`;
|
||||
}
|
||||
return relay;
|
||||
}).filter(relay => {
|
||||
// Remove duplicates and validate URLs
|
||||
try {
|
||||
new URL(relay);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}).filter((relay, index, self) => self.indexOf(relay) === index); // dedupe
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal NIP-44 codec fallback
|
||||
class Nip44 {
|
||||
constructor() {
|
||||
this.Nip44 = null;
|
||||
// Initialize with existing codec if available
|
||||
this.nip44Available = window.NostrTools?.nip44;
|
||||
}
|
||||
|
||||
static encrypt(ourSk, theirPk, plaintext) {
|
||||
if (window.NostrTools?.nip44?.encrypt) {
|
||||
return window.NostrTools.nip44.encrypt(ourSk, theirPk, plaintext);
|
||||
}
|
||||
|
||||
throw new Error('NIP-44 encryption not available. Please use nostr-tools@>=2.x or provide codec implementation.');
|
||||
}
|
||||
|
||||
static decrypt(ourSk, theirPk, ciphertext) {
|
||||
if (window.NostrTools?.nip44?.decrypt) {
|
||||
return window.NostrTools.nip44.decrypt(ourSk, theirPk, ciphertext);
|
||||
}
|
||||
|
||||
throw new Error('NIP-44 decryption not available. Please use nostr-tools@>=2.x or provide codec implementation.');
|
||||
}
|
||||
}
|
||||
|
||||
// LocalSigner wrapping window.NostrTools
|
||||
class LocalSigner {
|
||||
constructor(sk) {
|
||||
this.sk = sk;
|
||||
// Generate pubkey from secret key
|
||||
this.pk = this._getPubKey();
|
||||
}
|
||||
|
||||
_getPubKey() {
|
||||
const seckey = this.sk.startsWith('nsec') ?
|
||||
window.NostrTools.nip19.decode(this.sk).data :
|
||||
this.sk;
|
||||
return window.NostrTools.getPublicKey(seckey);
|
||||
}
|
||||
|
||||
pubkey() {
|
||||
return this.pk;
|
||||
}
|
||||
|
||||
async sign(event) {
|
||||
// Prepare event for signing
|
||||
const ev = { ...event };
|
||||
ev.pubkey = this.pk;
|
||||
|
||||
// Generate event ID and sign
|
||||
const signedEvent = await window.NostrTools.finalizeEvent(ev, this.sk);
|
||||
return signedEvent;
|
||||
}
|
||||
|
||||
async encrypt04(pubkey, plaintext) {
|
||||
return await window.NostrTools.nip04.encrypt(this.sk, pubkey, plaintext);
|
||||
}
|
||||
|
||||
async decrypt04(pubkey, ciphertext) {
|
||||
return await window.NostrTools.nip04.decrypt(this.sk, pubkey, ciphertext);
|
||||
}
|
||||
|
||||
async encrypt44(pubkey, plaintext) {
|
||||
return Nip44.encrypt(this.sk, pubkey, plaintext);
|
||||
}
|
||||
|
||||
async decrypt44(pubkey, ciphertext) {
|
||||
return Nip44.decrypt(this.sk, pubkey, ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
// ExtensionBridge for detecting and managing browser extensions
|
||||
class ExtensionBridge {
|
||||
constructor() {
|
||||
this.checking = false;
|
||||
this.checkInterval = null;
|
||||
this.originalNostr = null;
|
||||
this.foundExtension = null;
|
||||
}
|
||||
|
||||
startChecking(nostrLite) {
|
||||
if (this.checking) return;
|
||||
this.checking = true;
|
||||
|
||||
const check = () => {
|
||||
this.initExtension(nostrLite);
|
||||
};
|
||||
|
||||
// Check immediately
|
||||
check();
|
||||
|
||||
// Then check every 200ms for 30 seconds
|
||||
this.checkInterval = setInterval(check, 200);
|
||||
|
||||
// Stop checking after 30 seconds
|
||||
setTimeout(() => {
|
||||
clearInterval(this.checkInterval);
|
||||
this.checkInterval = null;
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
initExtension(nostrLite, lastTry = false) {
|
||||
const extension = window.nostr;
|
||||
|
||||
if (extension && !this.foundExtension) {
|
||||
// Check if this is actually a real extension, not our own library
|
||||
const isRealExtension = (
|
||||
extension !== nostrLite && // Not the same object we're about to assign
|
||||
extension !== windowNostr && // Not our windowNostr object
|
||||
typeof extension._hexToUint8Array !== 'function' && // Our library has this internal method
|
||||
extension.constructor.name !== 'Object' // Real extensions usually have proper constructors
|
||||
);
|
||||
|
||||
if (isRealExtension) {
|
||||
this.foundExtension = extension;
|
||||
|
||||
// Cache the extension and reassign window.nostr to our lite version
|
||||
this.originalNostr = window.nostr;
|
||||
window.nostr = nostrLite;
|
||||
|
||||
console.log('Real Nostr extension detected and bridged:', extension.constructor.name);
|
||||
|
||||
// If currently authenticated, reconcile state
|
||||
if (LiteState.auth?.signer?.method === 'extension') {
|
||||
this.reconcileExtension();
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping non-extension object on window.nostr:', extension.constructor.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasExtension() {
|
||||
return !!this.foundExtension;
|
||||
}
|
||||
|
||||
async setExtensionReadPubkey(expectedPubkey = null) {
|
||||
if (!this.foundExtension) return false;
|
||||
|
||||
try {
|
||||
// Temporarily set window.nostr to extension
|
||||
const temp = window.nostr;
|
||||
window.nostr = this.foundExtension;
|
||||
|
||||
const pubkey = await this.foundExtension.getPublicKey();
|
||||
|
||||
// Restore our lite implementation
|
||||
window.nostr = temp;
|
||||
|
||||
if (expectedPubkey && pubkey !== expectedPubkey) {
|
||||
console.warn(`Extension pubkey ${pubkey} does not match expected ${expectedPubkey}`);
|
||||
}
|
||||
|
||||
return pubkey;
|
||||
} catch (e) {
|
||||
console.error('Error reading extension pubkey:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
trySetForPubkey(expectedPubkey) {
|
||||
if (!this.hasExtension()) return false;
|
||||
|
||||
this.setExtensionReadPubkey(expectedPubkey).then(pubkey => {
|
||||
if (pubkey) {
|
||||
LiteState.bus?.emit('extensionLogin', { pubkey });
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setExtension() {
|
||||
if (!this.foundExtension) return;
|
||||
window.nostr = this.foundExtension;
|
||||
this.setExtensionReadPubkey().then(pubkey => {
|
||||
if (pubkey) {
|
||||
LiteState.bus?.emit('extensionSet', { pubkey });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unset(nostrLite) {
|
||||
window.nostr = nostrLite;
|
||||
}
|
||||
|
||||
reconcileExtension() {
|
||||
// Handle extension state changes
|
||||
this.setExtensionReadPubkey().then(pubkey => {
|
||||
if (pubkey) {
|
||||
// Update current account if extension is the signer
|
||||
const current = Store.getCurrent();
|
||||
if (current && current.signer?.method === 'extension') {
|
||||
const info = {
|
||||
...current,
|
||||
pubkey,
|
||||
signer: { method: 'extension' }
|
||||
};
|
||||
Store.setCurrent(info);
|
||||
LiteState.bus?.emit('authStateUpdate', info);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Main API surface
|
||||
class NostrLite {
|
||||
static async init(options = {}) {
|
||||
// Ensure dependencies are loaded
|
||||
Deps.ensureNostrToolsLoaded();
|
||||
|
||||
// Prevent double initialization
|
||||
if (LiteState.initialized) {
|
||||
console.warn('NOSTR_LOGIN_LITE already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
LiteState.bus = new Bus();
|
||||
LiteState.extensionBridge = new ExtensionBridge();
|
||||
|
||||
// Initialize NIP-46 client
|
||||
LiteState.nip46Client = new NIP46Client();
|
||||
|
||||
// Store options
|
||||
LiteState.options = {
|
||||
theme: 'light',
|
||||
darkMode: false,
|
||||
relays: Relays.getDefaultRelays(options),
|
||||
methods: {
|
||||
connect: true,
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: true,
|
||||
otp: true
|
||||
},
|
||||
otp: {},
|
||||
...options
|
||||
};
|
||||
|
||||
// Start extension detection
|
||||
LiteState.extensionBridge.startChecking(windowNostr);
|
||||
|
||||
// Setup auth methods
|
||||
this._setupAuth();
|
||||
|
||||
// Initialize modal UI
|
||||
// this._initModal();
|
||||
|
||||
console.log('NOSTR_LOGIN_LITE initialized with options:', LiteState.options);
|
||||
LiteState.initialized = true;
|
||||
}
|
||||
|
||||
static _setupAuth() {
|
||||
// Set up event listeners for modal interactions
|
||||
window.addEventListener('nlMethodSelected', (event) => {
|
||||
this._handleMethodSelected(event.detail);
|
||||
});
|
||||
|
||||
// Set up other auth-related event listeners
|
||||
this._setupAuthEventListeners();
|
||||
|
||||
console.log('Auth system setup loaded');
|
||||
}
|
||||
|
||||
static _setupAuthEventListeners() {
|
||||
// Handle extension detection
|
||||
this.bus?.on('extensionDetected', (extension) => {
|
||||
console.log('Extension detected');
|
||||
LiteState.extensionBridge.foundExtension = extension;
|
||||
});
|
||||
|
||||
// Handle auth URL from NIP-46
|
||||
window.addEventListener('nlAuthUrl', (event) => {
|
||||
console.log('Auth URL received:', event.detail.url);
|
||||
// Could show URL in modal or trigger external flow
|
||||
});
|
||||
|
||||
// Handle logout events
|
||||
window.addEventListener('nlLogout', () => {
|
||||
console.log('Logout event received');
|
||||
this.logout();
|
||||
});
|
||||
}
|
||||
|
||||
static _handleMethodSelected(detail) {
|
||||
console.log('Method selected:', detail);
|
||||
|
||||
const { method, pubkey, secret, extension } = detail;
|
||||
|
||||
switch (method) {
|
||||
case 'local':
|
||||
if (secret && pubkey) {
|
||||
// Set up local key authentication
|
||||
const info = {
|
||||
pubkey,
|
||||
signer: { method: 'local', secret }
|
||||
};
|
||||
Store.setCurrent(info);
|
||||
LiteState.bus?.emit('authStateUpdate', info);
|
||||
this._dispatchAuthEvent('login', info);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'extension':
|
||||
if (pubkey && extension) {
|
||||
// Store the extension object in the ExtensionBridge for future use
|
||||
LiteState.extensionBridge.foundExtension = extension;
|
||||
LiteState.extensionBridge.originalNostr = extension;
|
||||
|
||||
// Set up extension authentication
|
||||
const info = {
|
||||
pubkey,
|
||||
signer: { method: 'extension' }
|
||||
};
|
||||
Store.setCurrent(info);
|
||||
LiteState.bus?.emit('authStateUpdate', info);
|
||||
this._dispatchAuthEvent('login', info);
|
||||
|
||||
console.log('Extension authentication set up successfully');
|
||||
} else {
|
||||
// Fallback to extension bridge detection
|
||||
LiteState.bus?.emit('authMethodSelected', { method: 'extension' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'readonly':
|
||||
// Set read-only mode
|
||||
const readonlyInfo = {
|
||||
pubkey: '',
|
||||
signer: { method: 'readonly' }
|
||||
};
|
||||
Store.setCurrent(readonlyInfo);
|
||||
LiteState.bus?.emit('authStateUpdate', readonlyInfo);
|
||||
this._dispatchAuthEvent('login', readonlyInfo);
|
||||
break;
|
||||
|
||||
case 'nip46':
|
||||
if (secret && pubkey) {
|
||||
// Set up NIP-46 remote signing
|
||||
const info = {
|
||||
pubkey,
|
||||
signer: { method: 'nip46', ...secret }
|
||||
};
|
||||
Store.setCurrent(info);
|
||||
LiteState.bus?.emit('authStateUpdate', info);
|
||||
this._dispatchAuthEvent('login', info);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unhandled auth method:', method);
|
||||
}
|
||||
}
|
||||
|
||||
static _dispatchAuthEvent(type, info) {
|
||||
const eventPayload = {
|
||||
type,
|
||||
info,
|
||||
pubkey: info?.pubkey || '',
|
||||
method: info?.signer?.method || '',
|
||||
...info
|
||||
};
|
||||
|
||||
// Dispatch the event
|
||||
window.dispatchEvent(new CustomEvent('nlAuth', { detail: eventPayload }));
|
||||
|
||||
this.bus?.emit('nlAuth', eventPayload);
|
||||
}
|
||||
|
||||
static launch(startScreen) {
|
||||
if (!LiteState.initialized) {
|
||||
throw new Error('NOSTR_LOGIN_LITE not initialized. Call init() first.');
|
||||
}
|
||||
|
||||
console.log('Launch requested with screen:', startScreen);
|
||||
|
||||
// Initialize modal if needed
|
||||
if (!LiteState.modal) {
|
||||
// Import modal lazily
|
||||
if (typeof Modal !== 'undefined') {
|
||||
LiteState.modal = Modal.init(LiteState.options);
|
||||
} else {
|
||||
console.error('Modal component not available');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Open modal with specified screen
|
||||
LiteState.modal.open({ startScreen });
|
||||
}
|
||||
|
||||
static logout() {
|
||||
if (!LiteState.initialized) return;
|
||||
|
||||
// Clear current account and state
|
||||
Store.removeCurrentAccount();
|
||||
|
||||
// Reset internal state
|
||||
LiteState.auth = null;
|
||||
|
||||
// Emit logout event
|
||||
window.dispatchEvent(new CustomEvent('nlLogout'));
|
||||
LiteState.bus?.emit('logout');
|
||||
|
||||
console.log('Logged out');
|
||||
}
|
||||
|
||||
static setDarkMode(dark) {
|
||||
if (!LiteState.options) return;
|
||||
|
||||
LiteState.options.darkMode = dark;
|
||||
Store.setItem('darkMode', dark.toString());
|
||||
|
||||
// Update modal theme if initialized
|
||||
if (LiteState.modal) {
|
||||
// LiteState.modal.updateTheme();
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('nlDarkMode', { detail: { dark } }));
|
||||
}
|
||||
|
||||
static setAuth(o) {
|
||||
if (!o || !o.type) return;
|
||||
|
||||
console.log('setAuth called:', o);
|
||||
|
||||
// Validate request
|
||||
if (!['login', 'signup', 'logout'].includes(o.type)) {
|
||||
throw new Error(`Invalid auth type: ${o.type}`);
|
||||
}
|
||||
|
||||
if (['login', 'signup'].includes(o.type) && !['connect', 'extension', 'local', 'otp', 'readOnly'].includes(o.method)) {
|
||||
throw new Error(`Invalid auth method: ${o.method}`);
|
||||
}
|
||||
|
||||
// Handle based on type
|
||||
switch (o.type) {
|
||||
case 'logout':
|
||||
this.logout();
|
||||
break;
|
||||
default:
|
||||
// Delegate to auth system - will be implemented
|
||||
console.log('Auth delegation not yet implemented');
|
||||
}
|
||||
}
|
||||
|
||||
static cancelNeedAuth() {
|
||||
// Cancel any ongoing auth flows
|
||||
LiteState.bus?.emit('cancelAuth');
|
||||
console.log('Auth flow cancelled');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the window.nostr facade
|
||||
const windowNostr = {
|
||||
async getPublicKey() {
|
||||
if (!LiteState.initialized) {
|
||||
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
||||
}
|
||||
|
||||
const current = Store.getCurrent();
|
||||
if (current && current.pubkey) {
|
||||
return current.pubkey;
|
||||
}
|
||||
|
||||
// Trigger auth flow
|
||||
const authPromise = new Promise((resolve, reject) => {
|
||||
const handleAuth = (event) => {
|
||||
window.removeEventListener('nlAuth', handleAuth);
|
||||
if (event.detail.type === 'login' && event.detail.pubkey) {
|
||||
resolve(event.detail.pubkey);
|
||||
} else {
|
||||
reject(new Error('Authentication cancelled'));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('nlAuth', handleAuth);
|
||||
|
||||
// Set timeout
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('nlAuth', handleAuth);
|
||||
reject(new Error('Authentication timeout'));
|
||||
}, 300000); // 5 minutes
|
||||
});
|
||||
|
||||
// Launch auth modal
|
||||
NostrLite.launch('login');
|
||||
|
||||
return authPromise;
|
||||
},
|
||||
|
||||
async signEvent(event) {
|
||||
if (!LiteState.initialized) {
|
||||
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
||||
}
|
||||
|
||||
let current = Store.getCurrent();
|
||||
|
||||
// If no current account, trigger auth
|
||||
if (!current) {
|
||||
await window.nostr.getPublicKey(); // This will trigger auth
|
||||
current = Store.getCurrent();
|
||||
if (!current) {
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Route to appropriate signer
|
||||
if (current.signer?.method === 'local' && current.signer.secret) {
|
||||
const signer = new LocalSigner(this._hexToUint8Array(current.signer.secret));
|
||||
return await signer.sign(event);
|
||||
} else if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
||||
// Route to NIP-46 remote signer
|
||||
try {
|
||||
const bunkerSigner = current.signer.bunkerSigner;
|
||||
const signedEvent = await bunkerSigner.signEvent(event);
|
||||
return signedEvent;
|
||||
} catch (error) {
|
||||
console.error('NIP-46 signEvent failed:', error);
|
||||
throw new Error(`NIP-46 signing failed: ${error.message}`);
|
||||
}
|
||||
} else if (current.signer?.method === 'readonly') {
|
||||
throw new Error('Cannot sign events in read-only mode');
|
||||
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
||||
// Route to extension
|
||||
const temp = window.nostr;
|
||||
window.nostr = LiteState.extensionBridge.foundExtension;
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
return signedEvent;
|
||||
} finally {
|
||||
window.nostr = temp;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No suitable signer available for current account');
|
||||
},
|
||||
|
||||
_hexToUint8Array(hex) {
|
||||
// Convert hex string to Uint8Array
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
},
|
||||
|
||||
nip04: {
|
||||
async encrypt(pubkey, plaintext) {
|
||||
if (!LiteState.initialized) {
|
||||
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
||||
}
|
||||
|
||||
const current = Store.getCurrent();
|
||||
if (!current) {
|
||||
throw new Error('No authenticated user');
|
||||
}
|
||||
|
||||
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
||||
// Route to NIP-46 remote signer
|
||||
try {
|
||||
const bunkerSigner = current.signer.bunkerSigner;
|
||||
return await bunkerSigner.nip04Encrypt(pubkey, plaintext);
|
||||
} catch (error) {
|
||||
console.error('NIP-46 nip04 encrypt failed:', error);
|
||||
throw new Error(`NIP-46 encrypting failed: ${error.message}`);
|
||||
}
|
||||
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
||||
const signer = new LocalSigner(current.signer.secret);
|
||||
return await signer.encrypt04(pubkey, plaintext);
|
||||
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
||||
const temp = window.nostr;
|
||||
window.nostr = LiteState.extensionBridge.foundExtension;
|
||||
try {
|
||||
return await window.nostr.nip04.encrypt(pubkey, plaintext);
|
||||
} finally {
|
||||
window.nostr = temp;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No suitable signer available for NIP-04 encryption');
|
||||
},
|
||||
|
||||
async decrypt(pubkey, ciphertext) {
|
||||
if (!LiteState.initialized) {
|
||||
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
||||
}
|
||||
|
||||
const current = Store.getCurrent();
|
||||
if (!current) {
|
||||
throw new Error('No authenticated user');
|
||||
}
|
||||
|
||||
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
||||
// Route to NIP-46 remote signer
|
||||
try {
|
||||
const bunkerSigner = current.signer.bunkerSigner;
|
||||
return await bunkerSigner.nip04Decrypt(pubkey, ciphertext);
|
||||
} catch (error) {
|
||||
console.error('NIP-46 nip04 decrypt failed:', error);
|
||||
throw new Error(`NIP-46 decrypting failed: ${error.message}`);
|
||||
}
|
||||
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
||||
const signer = new LocalSigner(current.signer.secret);
|
||||
return await signer.decrypt04(pubkey, ciphertext);
|
||||
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
||||
const temp = window.nostr;
|
||||
window.nostr = LiteState.extensionBridge.foundExtension;
|
||||
try {
|
||||
return await window.nostr.nip04.decrypt(pubkey, ciphertext);
|
||||
} finally {
|
||||
window.nostr = temp;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No suitable signer available for NIP-04 decryption');
|
||||
}
|
||||
},
|
||||
|
||||
nip44: {
|
||||
async encrypt(pubkey, plaintext) {
|
||||
if (!LiteState.initialized) {
|
||||
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
||||
}
|
||||
|
||||
const current = Store.getCurrent();
|
||||
if (!current) {
|
||||
throw new Error('No authenticated user');
|
||||
}
|
||||
|
||||
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
||||
// Route to NIP-46 remote signer
|
||||
try {
|
||||
const bunkerSigner = current.signer.bunkerSigner;
|
||||
return await bunkerSigner.nip44Encrypt(pubkey, plaintext);
|
||||
} catch (error) {
|
||||
console.error('NIP-46 nip44 encrypt failed:', error);
|
||||
throw new Error(`NIP-46 encrypting failed: ${error.message}`);
|
||||
}
|
||||
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
||||
const signer = new LocalSigner(current.signer.secret);
|
||||
return await signer.encrypt44(pubkey, plaintext);
|
||||
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
||||
// Use extension if it supports nip44
|
||||
const temp = window.nostr;
|
||||
window.nostr = LiteState.extensionBridge.foundExtension;
|
||||
try {
|
||||
if (window.nostr.nip44) {
|
||||
return await window.nostr.nip44.encrypt(pubkey, plaintext);
|
||||
} else {
|
||||
throw new Error('Extension does not support NIP-44');
|
||||
}
|
||||
} finally {
|
||||
window.nostr = temp;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No suitable signer available for NIP-44 encryption');
|
||||
},
|
||||
|
||||
async decrypt(pubkey, ciphertext) {
|
||||
if (!LiteState.initialized) {
|
||||
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
||||
}
|
||||
|
||||
const current = Store.getCurrent();
|
||||
if (!current) {
|
||||
throw new Error('No authenticated user');
|
||||
}
|
||||
|
||||
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
||||
// Route to NIP-46 remote signer
|
||||
try {
|
||||
const bunkerSigner = current.signer.bunkerSigner;
|
||||
return await bunkerSigner.nip44Decrypt(pubkey, ciphertext);
|
||||
} catch (error) {
|
||||
console.error('NIP-46 nip44 decrypt failed:', error);
|
||||
throw new Error(`NIP-46 decrypting failed: ${error.message}`);
|
||||
}
|
||||
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
||||
const signer = new LocalSigner(current.signer.secret);
|
||||
return await signer.decrypt44(pubkey, ciphertext);
|
||||
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
||||
const temp = window.nostr;
|
||||
window.nostr = LiteState.extensionBridge.foundExtension;
|
||||
try {
|
||||
if (window.nostr.nip44) {
|
||||
return await window.nostr.nip44.decrypt(pubkey, ciphertext);
|
||||
} else {
|
||||
throw new Error('Extension does not support NIP-44');
|
||||
}
|
||||
} finally {
|
||||
window.nostr = temp;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No suitable signer available for NIP-44 decryption');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export the API
|
||||
window.NOSTR_LOGIN_LITE = {
|
||||
init: NostrLite.init.bind(NostrLite),
|
||||
launch: NostrLite.launch.bind(NostrLite),
|
||||
logout: NostrLite.logout.bind(NostrLite),
|
||||
setDarkMode: NostrLite.setDarkMode.bind(NostrLite),
|
||||
setAuth: NostrLite.setAuth.bind(NostrLite),
|
||||
cancelNeedAuth: NostrLite.cancelNeedAuth.bind(NostrLite),
|
||||
// Expose internal components for debugging
|
||||
get _extensionBridge() {
|
||||
return LiteState.extensionBridge;
|
||||
},
|
||||
get _state() {
|
||||
return LiteState;
|
||||
}
|
||||
};
|
||||
|
||||
// Set window.nostr facade properly (extensions will be handled by ExtensionBridge)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.nostr = windowNostr;
|
||||
|
||||
// Ensure all methods are properly exposed
|
||||
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed with methods:', Object.keys(windowNostr));
|
||||
}
|
||||
|
||||
console.log('NOSTR_LOGIN_LITE loaded - use window.NOSTR_LOGIN_LITE.init(options) to initialize');
|
||||
6860
lite/nostr.bundle.js
Normal file
6860
lite/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
1009
lite/ui/modal.js
Normal file
1009
lite/ui/modal.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user