Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad361a686 | ||
|
|
025d66c096 |
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 = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
2541
lite/nostr-login-lite.bundle.js
Normal file
2541
lite/nostr-login-lite.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
1039
lite/nostr-login-lite.js
Normal file
1039
lite/nostr-login-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
6860
lite/nostr.bundle.js
Normal file
6860
lite/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
1090
lite/ui/modal.js
Normal file
1090
lite/ui/modal.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user