1 Commits

Author SHA1 Message Date
Your Name
517974699d browser extension signing fixed 2025-09-15 13:51:41 -04:00
2 changed files with 593 additions and 51 deletions

View File

@@ -1093,10 +1093,8 @@ class NostrLite {
// Apply the selected theme (CSS-only)
this.switchTheme(this.options.theme);
// Set up window.nostr facade if no extension detected
if (this.extensionBridge.getExtensionCount() === 0) {
this._setupWindowNostrFacade();
}
// Always set up window.nostr facade to handle multiple extensions properly
this._setupWindowNostrFacade();
// Create modal during init (matching original git architecture)
this.modal = new Modal(this.options);
@@ -1115,9 +1113,14 @@ class NostrLite {
}
_setupWindowNostrFacade() {
if (typeof window !== 'undefined' && !window.nostr) {
window.nostr = new WindowNostr(this);
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed');
if (typeof window !== 'undefined') {
// Store existing window.nostr if it exists (from extensions)
const existingNostr = window.nostr;
// Always install our facade
window.nostr = new WindowNostr(this, existingNostr);
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed',
existingNostr ? '(with extension passthrough)' : '(no existing extension)');
}
}
@@ -1231,45 +1234,313 @@ class NostrLite {
}
}
// Window.nostr facade for when no extension is available
// NIP-07 compliant window.nostr provider
class WindowNostr {
constructor(nostrLite) {
constructor(nostrLite, existingNostr = null) {
this.nostrLite = nostrLite;
this.authState = null;
this.existingNostr = existingNostr;
this.authenticatedExtension = null;
this._setupEventListeners();
}
_setupEventListeners() {
// Listen for authentication events to store auth state
if (typeof window !== 'undefined') {
window.addEventListener('nlMethodSelected', (event) => {
this.authState = event.detail;
// If extension method, capture the specific extension the user chose
if (event.detail.method === 'extension') {
this.authenticatedExtension = event.detail.extension;
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
// Re-install our facade to ensure we intercept signEvent calls
// Extensions may overwrite window.nostr after authentication
if (typeof window !== 'undefined') {
console.log('WindowNostr: Re-installing facade after authentication');
window.nostr = this;
}
}
console.log('WindowNostr: Auth state updated:', this.authState?.method);
});
window.addEventListener('nlLogout', () => {
this.authState = null;
this.authenticatedExtension = null;
console.log('WindowNostr: Auth state cleared');
});
}
}
async getPublicKey() {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
switch (this.authState.method) {
case 'extension':
// Use the captured authenticated extension, not current window.nostr
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.getPublicKey();
case 'local':
case 'nip46':
return this.authState.pubkey;
case 'readonly':
throw new Error('Read-only mode - cannot get public key');
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
}
}
async signEvent(event) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot sign events');
}
switch (this.authState.method) {
case 'extension':
// Use the captured authenticated extension, not current window.nostr
console.log('WindowNostr: signEvent - authenticatedExtension:', this.authenticatedExtension);
console.log('WindowNostr: signEvent - authState.extension:', this.authState.extension);
console.log('WindowNostr: signEvent - existingNostr:', this.existingNostr);
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
console.log('WindowNostr: signEvent - using extension:', ext);
console.log('WindowNostr: signEvent - extension constructor:', ext?.constructor?.name);
if (!ext) throw new Error('Extension not available');
return await ext.signEvent(event);
case 'local': {
// Use nostr-tools to sign with local secret key
const { nip19, finalizeEvent } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
// Convert hex to Uint8Array
secretKey = this._hexToUint8Array(this.authState.secret);
}
return finalizeEvent(event, secretKey);
}
case 'nip46': {
// Use BunkerSigner for NIP-46
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.signEvent(event);
}
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
}
}
async getRelays() {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
// Return configured relays from nostr-lite options
return this.nostrLite.options?.relays || ['wss://relay.damus.io'];
}
get nip04() {
return {
async encrypt(pubkey, plaintext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip04.encrypt(pubkey, plaintext);
}
case 'local': {
const { nip04, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return await nip04.encrypt(secretKey, pubkey, plaintext);
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip04Encrypt(pubkey, plaintext);
}
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
}
},
async decrypt(pubkey, ciphertext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip04.decrypt(pubkey, ciphertext);
}
case 'local': {
const { nip04, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return await nip04.decrypt(secretKey, pubkey, ciphertext);
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip04Decrypt(pubkey, ciphertext);
}
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
}
}
};
}
get nip44() {
return {
async encrypt(pubkey, plaintext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.encrypt(pubkey, plaintext);
}
case 'local': {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
}
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
}
},
async decrypt(pubkey, ciphertext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.decrypt(pubkey, ciphertext);
}
case 'local': {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
}
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
}
}
};
}
_hexToUint8Array(hex) {
if (hex.length % 2 !== 0) {
throw new Error('Invalid hex string length');
}
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;
}
}
// Initialize and export

View File

@@ -8,7 +8,7 @@
* Two-file architecture:
* 1. Load nostr.bundle.js (official nostr-tools bundle)
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
* Generated on: 2025-09-14T20:06:51.995Z
* Generated on: 2025-09-15T17:48:16.817Z
*/
// Verify dependencies are loaded
@@ -2401,10 +2401,8 @@ class NostrLite {
// Apply the selected theme (CSS-only)
this.switchTheme(this.options.theme);
// Set up window.nostr facade if no extension detected
if (this.extensionBridge.getExtensionCount() === 0) {
this._setupWindowNostrFacade();
}
// Always set up window.nostr facade to handle multiple extensions properly
this._setupWindowNostrFacade();
// Create modal during init (matching original git architecture)
this.modal = new Modal(this.options);
@@ -2423,9 +2421,14 @@ class NostrLite {
}
_setupWindowNostrFacade() {
if (typeof window !== 'undefined' && !window.nostr) {
window.nostr = new WindowNostr(this);
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed');
if (typeof window !== 'undefined') {
// Store existing window.nostr if it exists (from extensions)
const existingNostr = window.nostr;
// Always install our facade
window.nostr = new WindowNostr(this, existingNostr);
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed',
existingNostr ? '(with extension passthrough)' : '(no existing extension)');
}
}
@@ -2539,45 +2542,313 @@ class NostrLite {
}
}
// Window.nostr facade for when no extension is available
// NIP-07 compliant window.nostr provider
class WindowNostr {
constructor(nostrLite) {
constructor(nostrLite, existingNostr = null) {
this.nostrLite = nostrLite;
this.authState = null;
this.existingNostr = existingNostr;
this.authenticatedExtension = null;
this._setupEventListeners();
}
_setupEventListeners() {
// Listen for authentication events to store auth state
if (typeof window !== 'undefined') {
window.addEventListener('nlMethodSelected', (event) => {
this.authState = event.detail;
// If extension method, capture the specific extension the user chose
if (event.detail.method === 'extension') {
this.authenticatedExtension = event.detail.extension;
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
// Re-install our facade to ensure we intercept signEvent calls
// Extensions may overwrite window.nostr after authentication
if (typeof window !== 'undefined') {
console.log('WindowNostr: Re-installing facade after authentication');
window.nostr = this;
}
}
console.log('WindowNostr: Auth state updated:', this.authState?.method);
});
window.addEventListener('nlLogout', () => {
this.authState = null;
this.authenticatedExtension = null;
console.log('WindowNostr: Auth state cleared');
});
}
}
async getPublicKey() {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
switch (this.authState.method) {
case 'extension':
// Use the captured authenticated extension, not current window.nostr
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.getPublicKey();
case 'local':
case 'nip46':
return this.authState.pubkey;
case 'readonly':
throw new Error('Read-only mode - cannot get public key');
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
}
}
async signEvent(event) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot sign events');
}
switch (this.authState.method) {
case 'extension':
// Use the captured authenticated extension, not current window.nostr
console.log('WindowNostr: signEvent - authenticatedExtension:', this.authenticatedExtension);
console.log('WindowNostr: signEvent - authState.extension:', this.authState.extension);
console.log('WindowNostr: signEvent - existingNostr:', this.existingNostr);
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
console.log('WindowNostr: signEvent - using extension:', ext);
console.log('WindowNostr: signEvent - extension constructor:', ext?.constructor?.name);
if (!ext) throw new Error('Extension not available');
return await ext.signEvent(event);
case 'local': {
// Use nostr-tools to sign with local secret key
const { nip19, finalizeEvent } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
// Convert hex to Uint8Array
secretKey = this._hexToUint8Array(this.authState.secret);
}
return finalizeEvent(event, secretKey);
}
case 'nip46': {
// Use BunkerSigner for NIP-46
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.signEvent(event);
}
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
}
}
async getRelays() {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
// Return configured relays from nostr-lite options
return this.nostrLite.options?.relays || ['wss://relay.damus.io'];
}
get nip04() {
return {
async encrypt(pubkey, plaintext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip04.encrypt(pubkey, plaintext);
}
case 'local': {
const { nip04, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return await nip04.encrypt(secretKey, pubkey, plaintext);
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip04Encrypt(pubkey, plaintext);
}
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
}
},
async decrypt(pubkey, ciphertext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip04.decrypt(pubkey, ciphertext);
}
case 'local': {
const { nip04, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return await nip04.decrypt(secretKey, pubkey, ciphertext);
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip04Decrypt(pubkey, ciphertext);
}
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
}
}
};
}
get nip44() {
return {
async encrypt(pubkey, plaintext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.encrypt(pubkey, plaintext);
}
case 'local': {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
}
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
}
},
async decrypt(pubkey, ciphertext) {
throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()');
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.decrypt(pubkey, ciphertext);
}
case 'local': {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
}
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
}
}
};
}
_hexToUint8Array(hex) {
if (hex.length % 2 !== 0) {
throw new Error('Invalid hex string length');
}
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;
}
}
// Initialize and export