upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-27 10:07:30 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-27 14:24:57 +0000
commit2c48e37f8341e0d207dd3260c439a0729464b03d (patch)
treee3188f6ff8b90e2b883335d95750fe69e16df361
parent436b707b2bdecb995bbdb374a029714c9f4c5159 (diff)
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
-rw-r--r--CHANGELOG.md1
-rw-r--r--src/bin/ngit/sub_commands/login.rs44
-rw-r--r--src/lib/login/fresh.rs56
3 files changed, 89 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 930fe39..607afc8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14- Fetch filters now request kind-5 deletion events for cached state and repo announcement events by `#e` tag (NIP-09), in addition to the existing `#a`-tagged filter; ensures deletions of these events are received even from clients that do not embed a repo coordinate in their deletion event 14- Fetch filters now request kind-5 deletion events for cached state and repo announcement events by `#e` tag (NIP-09), in addition to the existing `#a`-tagged filter; ensures deletions of these events are received even from clients that do not embed a repo coordinate in their deletion event
15- `FetchReport` now tracks and displays a count of kind-5 deletion events received (e.g. `"1 deletion"` in the fetch summary) 15- `FetchReport` now tracks and displays a count of kind-5 deletion events received (e.g. `"1 deletion"` in the fetch summary)
16- `ngit account login` nostrconnect flow now shows current signer relays and allows changing them 16- `ngit account login` nostrconnect flow now shows current signer relays and allows changing them
17- `ngit account login --bunker-url` - specify bunker URL for non-interactive nostrconnect login
17 18
18### Fixed 19### Fixed
19 20
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::{
11 cli::{Cli, extract_signer_cli_arguments}, 11 cli::{Cli, extract_signer_cli_arguments},
12 client::{Client, Connect}, 12 client::{Client, Connect},
13 git::Repo, 13 git::Repo,
14 login::fresh::fresh_login_or_signup, 14 login::fresh::{fresh_login_or_signup, login_with_bunker_url},
15}; 15};
16 16
17#[derive(clap::Args)] 17#[derive(clap::Args)]
@@ -27,22 +27,31 @@ pub struct SubCommandArgs {
27 /// signer relay for nostrconnect (can be used multiple times) 27 /// signer relay for nostrconnect (can be used multiple times)
28 #[arg(long = "signer-relay")] 28 #[arg(long = "signer-relay")]
29 signer_relays: Vec<String>, 29 signer_relays: Vec<String>,
30
31 /// bunker:// URL from signer app for non-interactive remote signer login
32 #[arg(long = "bunker-url")]
33 bunker_url: Option<String>,
30} 34}
31 35
32pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { 36pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
33 // Early validation: check if we have required parameters in non-interactive 37 // Early validation: check if we have required parameters in non-interactive
34 // mode 38 // mode
35 let signer_info = extract_signer_cli_arguments(args)?; 39 let signer_info = extract_signer_cli_arguments(args)?;
36 if Interactor::is_non_interactive() && signer_info.is_none() { 40 if Interactor::is_non_interactive()
41 && signer_info.is_none()
42 && command_args.bunker_url.is_none()
43 {
37 use ngit::cli_interactor::cli_error; 44 use ngit::cli_interactor::cli_error;
38 return Err(cli_error( 45 return Err(cli_error(
39 "requires --nsec or --interactive", 46 "requires --nsec, --bunker-url, or --interactive",
40 &[ 47 &[
41 ("--nsec <key>", "provide secret key (nsec or hex)"), 48 ("--nsec <key>", "provide secret key (nsec or hex)"),
42 ("--interactive", "for nostr connect or bunker login"), 49 ("--bunker-url <url>", "bunker:// URL from signer app"),
50 ("--interactive", "for interactive nostr connect login"),
43 ], 51 ],
44 &[ 52 &[
45 "ngit account login --nsec <your-nsec>", 53 "ngit account login --nsec <your-nsec>",
54 "ngit account login --bunker-url <bunker-url>",
46 "ngit account create", 55 "ngit account create",
47 ], 56 ],
48 )); 57 ));
@@ -61,14 +70,25 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
61 70
62 let (logged_out, log_in_locally_only) = logout(git_repo.as_ref(), command_args.local).await?; 71 let (logged_out, log_in_locally_only) = logout(git_repo.as_ref(), command_args.local).await?;
63 if logged_out || log_in_locally_only { 72 if logged_out || log_in_locally_only {
64 fresh_login_or_signup( 73 if let Some(bunker_url) = &command_args.bunker_url {
65 &git_repo.as_ref(), 74 login_with_bunker_url(
66 client.as_ref(), 75 &git_repo.as_ref(),
67 signer_info, 76 client.as_ref(),
68 log_in_locally_only || command_args.local, 77 bunker_url,
69 &command_args.signer_relays, 78 log_in_locally_only || command_args.local,
70 ) 79 &command_args.signer_relays,
71 .await?; 80 )
81 .await?;
82 } else {
83 fresh_login_or_signup(
84 &git_repo.as_ref(),
85 client.as_ref(),
86 signer_info,
87 log_in_locally_only || command_args.local,
88 &command_args.signer_relays,
89 )
90 .await?;
91 }
72 } 92 }
73 93
74 // If not offline, disconnect the client 94 // 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(
112 Ok((signer, user_ref, source)) 112 Ok((signer, user_ref, source))
113} 113}
114 114
115/// Non-interactive login using a `bunker://` URL provided directly.
116///
117/// Parses the URL, generates a fresh app key, connects to the remote signer,
118/// and saves the resulting credentials to git config.
119pub async fn login_with_bunker_url(
120 git_repo: &Option<&Repo>,
121 #[cfg(test)] client: Option<&MockConnect>,
122 #[cfg(not(test))] client: Option<&Client>,
123 bunker_url: &str,
124 save_local: bool,
125 signer_relays: &[String],
126) -> Result<(Arc<dyn NostrSigner>, UserRef, SignerInfoSource)> {
127 let url = NostrConnectURI::parse(bunker_url)
128 .context("invalid bunker:// URL - must be a valid bunker:// URI")?;
129
130 let (app_key, _) = generate_nostr_connect_app(client, signer_relays)?;
131
132 let printer = Arc::new(Mutex::new(Printer::default()));
133 {
134 let mut p = printer.lock().await;
135 p.println("connecting to remote signer...".to_string());
136 }
137
138 let (signer, user_public_key, bunker_uri) =
139 listen_for_remote_signer(&app_key, &url, printer).await?;
140
141 let signer_info = SignerInfo::Bunker {
142 bunker_uri: bunker_uri.to_string(),
143 bunker_app_key: app_key.secret_key().to_secret_hex(),
144 npub: Some(user_public_key.to_bech32()?),
145 };
146
147 let _ = save_to_git_config(git_repo, &signer_info, !save_local).await;
148
149 let user_ref = get_user_details(
150 &user_public_key,
151 client,
152 if let Some(git_repo) = git_repo {
153 Some(git_repo.get_path()?)
154 } else {
155 None
156 },
157 false,
158 false,
159 )
160 .await?;
161
162 let source = if save_local {
163 SignerInfoSource::GitLocal
164 } else {
165 SignerInfoSource::GitGlobal
166 };
167 print_logged_in_as(&user_ref, client.is_none(), &source)?;
168 Ok((signer, user_ref, source))
169}
170
115pub async fn get_fresh_nsec_signer() -> Result< 171pub async fn get_fresh_nsec_signer() -> Result<
116 Option<( 172 Option<(
117 Arc<dyn NostrSigner>, 173 Arc<dyn NostrSigner>,