From 2c48e37f8341e0d207dd3260c439a0729464b03d Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 27 Feb 2026 10:07:30 +0000 Subject: feat: add --bunker-url to account login for non-interactive use allows non-interactive bunker:// URL login without requiring --nsec, by connecting to the remote signer and saving credentials to git config --- src/bin/ngit/sub_commands/login.rs | 44 ++++++++++++++++++++++-------- src/lib/login/fresh.rs | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/bin/ngit/sub_commands/login.rs b/src/bin/ngit/sub_commands/login.rs index 3acfb66..29152ed 100644 --- a/src/bin/ngit/sub_commands/login.rs +++ b/src/bin/ngit/sub_commands/login.rs @@ -11,7 +11,7 @@ use crate::{ cli::{Cli, extract_signer_cli_arguments}, client::{Client, Connect}, git::Repo, - login::fresh::fresh_login_or_signup, + login::fresh::{fresh_login_or_signup, login_with_bunker_url}, }; #[derive(clap::Args)] @@ -27,22 +27,31 @@ pub struct SubCommandArgs { /// signer relay for nostrconnect (can be used multiple times) #[arg(long = "signer-relay")] signer_relays: Vec, + + /// bunker:// URL from signer app for non-interactive remote signer login + #[arg(long = "bunker-url")] + bunker_url: Option, } pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { // Early validation: check if we have required parameters in non-interactive // mode let signer_info = extract_signer_cli_arguments(args)?; - if Interactor::is_non_interactive() && signer_info.is_none() { + if Interactor::is_non_interactive() + && signer_info.is_none() + && command_args.bunker_url.is_none() + { use ngit::cli_interactor::cli_error; return Err(cli_error( - "requires --nsec or --interactive", + "requires --nsec, --bunker-url, or --interactive", &[ ("--nsec ", "provide secret key (nsec or hex)"), - ("--interactive", "for nostr connect or bunker login"), + ("--bunker-url ", "bunker:// URL from signer app"), + ("--interactive", "for interactive nostr connect login"), ], &[ "ngit account login --nsec ", + "ngit account login --bunker-url ", "ngit account create", ], )); @@ -61,14 +70,25 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { let (logged_out, log_in_locally_only) = logout(git_repo.as_ref(), command_args.local).await?; if logged_out || log_in_locally_only { - fresh_login_or_signup( - &git_repo.as_ref(), - client.as_ref(), - signer_info, - log_in_locally_only || command_args.local, - &command_args.signer_relays, - ) - .await?; + if let Some(bunker_url) = &command_args.bunker_url { + login_with_bunker_url( + &git_repo.as_ref(), + client.as_ref(), + bunker_url, + log_in_locally_only || command_args.local, + &command_args.signer_relays, + ) + .await?; + } else { + fresh_login_or_signup( + &git_repo.as_ref(), + client.as_ref(), + signer_info, + log_in_locally_only || command_args.local, + &command_args.signer_relays, + ) + .await?; + } } // If not offline, disconnect the client diff --git a/src/lib/login/fresh.rs b/src/lib/login/fresh.rs index 0b5922b..8d3a806 100644 --- a/src/lib/login/fresh.rs +++ b/src/lib/login/fresh.rs @@ -112,6 +112,62 @@ pub async fn fresh_login_or_signup( Ok((signer, user_ref, source)) } +/// Non-interactive login using a `bunker://` URL provided directly. +/// +/// Parses the URL, generates a fresh app key, connects to the remote signer, +/// and saves the resulting credentials to git config. +pub async fn login_with_bunker_url( + git_repo: &Option<&Repo>, + #[cfg(test)] client: Option<&MockConnect>, + #[cfg(not(test))] client: Option<&Client>, + bunker_url: &str, + save_local: bool, + signer_relays: &[String], +) -> Result<(Arc, UserRef, SignerInfoSource)> { + let url = NostrConnectURI::parse(bunker_url) + .context("invalid bunker:// URL - must be a valid bunker:// URI")?; + + let (app_key, _) = generate_nostr_connect_app(client, signer_relays)?; + + let printer = Arc::new(Mutex::new(Printer::default())); + { + let mut p = printer.lock().await; + p.println("connecting to remote signer...".to_string()); + } + + let (signer, user_public_key, bunker_uri) = + listen_for_remote_signer(&app_key, &url, printer).await?; + + let signer_info = SignerInfo::Bunker { + bunker_uri: bunker_uri.to_string(), + bunker_app_key: app_key.secret_key().to_secret_hex(), + npub: Some(user_public_key.to_bech32()?), + }; + + let _ = save_to_git_config(git_repo, &signer_info, !save_local).await; + + let user_ref = get_user_details( + &user_public_key, + client, + if let Some(git_repo) = git_repo { + Some(git_repo.get_path()?) + } else { + None + }, + false, + false, + ) + .await?; + + let source = if save_local { + SignerInfoSource::GitLocal + } else { + SignerInfoSource::GitGlobal + }; + print_logged_in_as(&user_ref, client.is_none(), &source)?; + Ok((signer, user_ref, source)) +} + pub async fn get_fresh_nsec_signer() -> Result< Option<( Arc, -- cgit v1.2.3