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