add NIP-07 browser extension signer support for wasm

This commit is contained in:
2026-10-24 20:29:09 +07:00
parent f829a2774d
commit f9b40d9d49
10 changed files with 251 additions and 51 deletions
+1
View File
@@ -28,6 +28,7 @@ sha2.workspace = true
data-encoding.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
nostr-browser-signer.workspace = true
nostr-memory.workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+51
View File
@@ -0,0 +1,51 @@
use anyhow::Result;
#[cfg(target_arch = "wasm32")]
use common::web_store;
use gpui::{App, Task};
use crate::USER_KEYRING;
/// Read the stored credential as `(username, secret)`
#[allow(clippy::type_complexity)]
pub(crate) fn read(cx: &App) -> Task<Result<Option<(String, Vec<u8>)>>> {
#[cfg(not(target_arch = "wasm32"))]
return cx.read_credentials(USER_KEYRING);
#[cfg(target_arch = "wasm32")]
{
let _ = cx;
Task::ready(
web_store::get(USER_KEYRING)
.map(|stored| stored.map(|content| (String::new(), content.into_bytes()))),
)
}
}
/// Write the stored credential
pub(crate) fn write(cx: &App, username: &str, secret: &[u8]) -> Task<Result<()>> {
#[cfg(not(target_arch = "wasm32"))]
return cx.write_credentials(USER_KEYRING, username, secret);
#[cfg(target_arch = "wasm32")]
{
let _ = (cx, username);
let content = match std::str::from_utf8(secret) {
Ok(content) => content,
Err(e) => return Task::ready(Err(anyhow::anyhow!("credential is not UTF-8: {e}"))),
};
Task::ready(web_store::set(USER_KEYRING, content))
}
}
/// Delete the stored credential
pub(crate) fn delete(cx: &App) -> Task<Result<()>> {
#[cfg(not(target_arch = "wasm32"))]
return cx.delete_credentials(USER_KEYRING);
#[cfg(target_arch = "wasm32")]
{
let _ = cx;
Task::ready(web_store::remove(USER_KEYRING))
}
}
+61 -13
View File
@@ -9,6 +9,8 @@ use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, TaskExt
#[cfg(not(target_arch = "wasm32"))]
use gpui_tokio::Tokio;
use instant::Duration;
#[cfg(target_arch = "wasm32")]
use nostr_browser_signer::BrowserSigner;
use nostr_connect::prelude::*;
use nostr_gossip_memory::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
@@ -19,6 +21,7 @@ use nostr_sdk::prelude::*;
mod blossom;
mod constants;
mod credentials;
mod file;
mod nip05;
mod nip4e;
@@ -138,9 +141,7 @@ impl NostrRegistry {
let _ = entity.update(cx, |this, cx| {
this.connect_bootstrap_relays(cx);
if cfg!(target_arch = "wasm32") {
cx.emit(StateEvent::NoSigner);
} else if let Some(secret) = cli_key {
if let Some(secret) = cli_key {
// Use CLI-provided key -- same path as restore_session
let keys = Keys::new(secret);
this.set_signer(keys, cx);
@@ -242,7 +243,7 @@ impl NostrRegistry {
/// Restore the signer from the stored credential
fn restore_session(&mut self, cx: &mut Context<Self>) {
let user_keyring = cx.read_credentials(USER_KEYRING);
let user_keyring = credentials::read(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = async {
@@ -279,6 +280,11 @@ impl NostrRegistry {
this.update(cx, |this, cx| {
this.connect_proxy(cx);
})?;
} else if content == "extension" {
#[cfg(target_arch = "wasm32")]
this.update(cx, |this, cx| {
this.connect_extension(cx);
})?;
} else {
this.update(cx, |_, cx| {
cx.emit(StateEvent::NoSigner);
@@ -314,7 +320,7 @@ impl NostrRegistry {
cx: &mut Context<Self>,
) -> Task<Result<PublicKey, Error>> {
let password = password.to_owned();
let user_keyring = cx.read_credentials(USER_KEYRING);
let user_keyring = credentials::read(cx);
cx.spawn(async move |this, cx| {
let content = user_keyring
@@ -373,9 +379,8 @@ impl NostrRegistry {
let (keys, ncryptsec) = job.await?;
let public_key = keys.public_key();
let write = cx.update(|cx| {
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), ncryptsec.as_bytes())
});
let write =
cx.update(|cx| credentials::write(cx, &public_key.to_hex(), ncryptsec.as_bytes()));
write.await?;
this.update(cx, |this, cx| {
@@ -412,9 +417,8 @@ impl NostrRegistry {
let keys = decrypt_task.await?;
let public_key = keys.public_key();
let write = cx.update(|cx| {
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), stored.as_bytes())
});
let write =
cx.update(|cx| credentials::write(cx, &public_key.to_hex(), stored.as_bytes()));
write.await?;
this.update(cx, |this, cx| {
@@ -440,7 +444,7 @@ impl NostrRegistry {
let keys = Keys::generate();
let credential = with_master_key(&uri_string, &keys);
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
let write = credentials::write(cx, "bunker", credential.as_bytes());
cx.spawn(async move |this, cx| {
let signer = NostrConnect::builder(connect_uri, keys)
@@ -464,12 +468,56 @@ impl NostrRegistry {
self.current_user = None;
self.passphrase_required = false;
cx.delete_credentials(USER_KEYRING).detach_and_log_err(cx);
credentials::delete(cx).detach_and_log_err(cx);
cx.emit(StateEvent::SignerChanged);
cx.emit(StateEvent::NoSigner);
cx.notify();
}
/// Connect the browser extension signer (NIP-07), remembering the choice so
/// reloads reconnect automatically
#[cfg(target_arch = "wasm32")]
pub fn connect_extension(&mut self, cx: &mut Context<Self>) {
let signer = match BrowserSigner::new() {
Ok(signer) => signer,
Err(e) => {
cx.emit(StateEvent::error(format!(
"No Nostr extension found: {e}. Install a NIP-07 extension like Alby or nos2x."
)));
cx.emit(StateEvent::NoSigner);
return;
}
};
self.tasks.push(cx.spawn(async move |this, cx| {
match signer.get_public_key_async().await {
Ok(public_key) => {
// Remember the choice so reloads reconnect automatically
this.update(cx, |this, cx| {
credentials::write(cx, "extension", b"extension").detach_and_log_err(cx);
this.signer.swap_inner(signer);
this.current_user = Some(public_key);
this.passphrase_required = false;
cx.emit(StateEvent::SignerChanged);
cx.notify();
})?;
}
Err(e) => {
// Drop the remembered choice so the next visit does not
// retry a signer that is no longer answering
this.update(cx, |_, cx| {
credentials::delete(cx).detach_and_log_err(cx);
cx.emit(StateEvent::error(e.to_string()));
cx.emit(StateEvent::NoSigner);
})?;
}
};
Ok(())
}));
}
/// Start the browser proxy
#[cfg(not(target_arch = "wasm32"))]
pub fn connect_proxy(&mut self, cx: &mut Context<Self>) {