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