feat(nip46): publish kind-0 metadata through the connected signer
This commit is contained in:
parent
327bf0ffe0
commit
6f4dbb2ca1
4 changed files with 108 additions and 4 deletions
|
|
@ -577,7 +577,7 @@ function RenameModal({
|
|||
perform: () => renameProfile(target.npub, trimmed),
|
||||
successMessage: (report) =>
|
||||
report.succeeded.length === 0 && report.failed.length === 0
|
||||
? `Renamed to "${trimmed}" (saved on this device; external-signer profiles publish their name from the signer app).`
|
||||
? `Renamed to "${trimmed}" and saved on this device. Use "Publish name" to announce it network-wide — Amber will ask you to approve.`
|
||||
: report.failed.length === 0
|
||||
? `Renamed to "${trimmed}" and published to ${report.succeeded.length} relay(s). It may take a minute to appear on other clients.`
|
||||
: `Renamed to "${trimmed}", but ${report.failed.length} relay(s) did not accept it. Use "Publish name" to retry.`,
|
||||
|
|
|
|||
20
src/ipc.rs
20
src/ipc.rs
|
|
@ -702,9 +702,23 @@ async fn run_with_app(app: &mut App, request: Request) -> Result<serde_json::Val
|
|||
}
|
||||
|
||||
Request::PublishProfileMetadata { npub } => {
|
||||
let key = app.vault_key().copied();
|
||||
let report =
|
||||
profiles::publish_profile_metadata(&app.vault, &npub, key.as_ref(), &app.settings)?;
|
||||
// Route through the profile's Signing source, exactly like
|
||||
// PublishNote: an embedded profile signs locally, a paired
|
||||
// profile's kind-0 is signed by the remote signer (Amber shows
|
||||
// an approval prompt), and a disconnected one fails closed.
|
||||
// The shared App guard is held across the round-trip; the
|
||||
// signer's demux needs no App lock to deliver the response.
|
||||
let signing = app.signing_for(&npub).await?;
|
||||
let stored = profiles::find_stored_profile(&app.vault, &npub)?.clone();
|
||||
let settings = app.settings.clone();
|
||||
let report = profiles::publish_metadata_signed(
|
||||
&settings,
|
||||
&stored.label,
|
||||
stored.picture.clone(),
|
||||
stored.nip05.clone(),
|
||||
&signing,
|
||||
)
|
||||
.await?;
|
||||
Ok(json!(report))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,11 +190,68 @@ pub struct MetadataPublishReport {
|
|||
pub failed: Vec<RelayFailure>,
|
||||
}
|
||||
|
||||
/// Publish a profile's stored label (and picture, when set) as kind 0 metadata
|
||||
/// through an explicit [`Signing`] source (embedded or external).
|
||||
///
|
||||
/// This is what the GUI "Publish name" path uses: for a paired profile the
|
||||
/// kind-0 event is signed by the remote signer (Amber shows an approval
|
||||
/// prompt), so the name becomes visible network-wide instead of staying a
|
||||
/// local vault label. A disconnected external profile fails closed with
|
||||
/// `ExternalSignerNotConnected` — never with a silent local-key fallback.
|
||||
pub async fn publish_metadata_signed(
|
||||
settings: &Settings,
|
||||
label: &str,
|
||||
picture: Option<String>,
|
||||
nip05: Option<String>,
|
||||
signing: &crate::signer::Signing,
|
||||
) -> Result<MetadataPublishReport, AppError> {
|
||||
let relay_urls = relays::enabled_urls(settings);
|
||||
if relay_urls.is_empty() {
|
||||
return Err(AppError::no_enabled_relays());
|
||||
}
|
||||
let mut metadata = Metadata::new().name(label).display_name(label);
|
||||
if let Some(picture) = &picture {
|
||||
if let Ok(parsed) = Url::parse(picture) {
|
||||
metadata = metadata.picture(parsed);
|
||||
}
|
||||
}
|
||||
if let Some(nip05) = &nip05 {
|
||||
metadata = metadata.nip05(nip05);
|
||||
}
|
||||
// Identity first (validates the signer controls this profile), then sign
|
||||
// through `Signing` — local key or the NIP-46 round-trip.
|
||||
let pubkey = signing.pubkey().await.map_err(AppError::from)?;
|
||||
let unsigned = EventBuilder::new(Kind::Metadata, metadata.as_json()).finalize_unsigned(pubkey);
|
||||
let signed = signing
|
||||
.sign(unsigned)
|
||||
.await
|
||||
.map_err(|e| AppError::sign_failed(format!("{e}")))?;
|
||||
|
||||
// Local signing can answer NIP-42 AUTH challenges; with an external
|
||||
// signer the app holds no key, so the pool opens without an
|
||||
// authenticator and auth-gated relays report per-relay.
|
||||
let client = match signing {
|
||||
crate::signer::Signing::Local(keys) => {
|
||||
relays::open_pool(keys.clone(), &relay_urls, None).await?
|
||||
}
|
||||
crate::signer::Signing::External { .. } => {
|
||||
relays::open_pool_anon(&relay_urls, None).await?
|
||||
}
|
||||
};
|
||||
let (succeeded, failed) =
|
||||
crate::publish::send_to_all_relays(&client, relay_urls, &signed, "metadata").await;
|
||||
Ok(MetadataPublishReport { succeeded, failed })
|
||||
}
|
||||
|
||||
/// Publish a profile's stored label (and picture, when set) as kind 0 metadata
|
||||
/// so external clients (Iris, Yakihonne, ...) display its name. Returns a
|
||||
/// per-relay report.
|
||||
///
|
||||
/// `key` must be the unlocked vault key when the vault is password-protected.
|
||||
///
|
||||
/// Local-only: always signs from the vault. The CLI uses this (it has no
|
||||
/// signer instances); GUI callers use [`publish_metadata_signed`] with an
|
||||
/// [`App::signing_for`] source so paired profiles sign remotely.
|
||||
pub fn publish_profile_metadata(
|
||||
vault: &Vault,
|
||||
npub: &str,
|
||||
|
|
|
|||
|
|
@ -648,6 +648,11 @@ async fn nip46_bunker_connect_params_match_spec_against_strict_amber() {
|
|||
));
|
||||
|
||||
let signer = Nip46ClientSigner::new(app.clone());
|
||||
// Register the handle on the App exactly like production's
|
||||
// `ensure_nip46_signer` does: `App::signing_for` (used below for the
|
||||
// kind-0 publish) resolves the live session through this handle.
|
||||
// `Nip46ClientSigner::clone` shares the session state.
|
||||
app.lock().await.nip46_signer = Some(std::sync::Arc::new(signer.clone()));
|
||||
|
||||
// No secret in the URI: the strict signer must still ack a well-formed
|
||||
// connect whose params[0] is its own pubkey.
|
||||
|
|
@ -705,6 +710,34 @@ async fn nip46_bunker_connect_params_match_spec_against_strict_amber() {
|
|||
);
|
||||
drop(app_guard);
|
||||
|
||||
// Kind-0 through the remote signer: the vault label becomes a signed
|
||||
// network-visible profile (what other clients display as the name).
|
||||
// Exercises the real GUI "Publish name" path — Signing::External with
|
||||
// identity validation — against the strict signer.
|
||||
let (settings, signing) = {
|
||||
let guard = app.lock().await;
|
||||
(
|
||||
guard.settings.clone(),
|
||||
guard
|
||||
.signing_for(&identity_npub)
|
||||
.await
|
||||
.expect("signing source for the paired profile"),
|
||||
)
|
||||
};
|
||||
let report = keynectr::profiles::publish_metadata_signed(
|
||||
&settings,
|
||||
"Strict Amber",
|
||||
None,
|
||||
None,
|
||||
&signing,
|
||||
)
|
||||
.await
|
||||
.expect("remote kind-0 publish");
|
||||
assert!(
|
||||
!report.succeeded.is_empty(),
|
||||
"at least one relay must accept the signed kind-0"
|
||||
);
|
||||
|
||||
signer.disconnect().await.ok();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue