use anyhow::{Context, Result, anyhow, bail};
use serde_json::{Value, json};
use crate::nip_w1::{self, PairKind};
use crate::nostr_event;
use crate::nostr_relay::Filter;
use crate::nostr_ws::NostrWs;
fn parse_npub_hex(s: &str) -> Result<[u8; 32]> {
let v = hex::decode(s).map_err(|_| anyhow!("npub must be 64-char hex (x-only key)"))?;
v.as_slice().try_into().map_err(|_| {
anyhow!(
"npub must be exactly 32 bytes (64 hex chars), got {}",
v.len()
)
})
}
fn require_transport_key() -> Result<[u8; 32]> {
crate::config::read_nostr_key()
.context("no Nostr transport key — run `wire enroll nostr` first")
}
fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn block_on<F: std::future::Future>(fut: F) -> Result<F::Output> {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
Ok(rt.block_on(fut))
}
pub(crate) fn cmd_nostr(cmd: super::NostrCommand) -> Result<()> {
match cmd {
super::NostrCommand::Pair { npub, relay, json } => cmd_pair(&npub, &relay, json),
super::NostrCommand::Fetch { relay, limit, json } => cmd_fetch(&relay, limit, json),
super::NostrCommand::Accept { npub, relay, json } => cmd_accept(&npub, &relay, json),
}
}
fn card_identity_pubkey(card: &Value) -> Result<Vec<u8>> {
let b64 = card
.get("verify_keys")
.and_then(Value::as_object)
.and_then(|m| m.values().next())
.and_then(|v| v.get("key"))
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("peer card missing verify_keys[*].key"))?;
crate::signing::b64decode(b64).map_err(|_| anyhow!("peer card verify key is not valid base64"))
}
fn cmd_accept(npub: &str, relay: &str, as_json: bool) -> Result<()> {
let nsk = require_transport_key()?;
let peer = parse_npub_hex(npub)?;
let my_xonly = crate::nostr_key::xonly_from_secret(&nsk)
.map_err(|e| anyhow!("transport key unusable: {e}"))?;
let my_card =
crate::config::read_agent_card().context("no agent card — run `wire up` first")?;
let filter = Filter {
authors: vec![npub.to_string()],
kinds: vec![nip_w1::PAIR_REQUEST_KIND],
p_tags: vec![hex::encode(my_xonly)],
limit: Some(20),
..Default::default()
};
let events = block_on(async {
let mut ws = NostrWs::connect(relay)
.await
.with_context(|| format!("connect {relay}"))?;
ws.pull(filter).await.context("pull pair-request")
})??;
let card = events
.iter()
.find_map(|ev| match nip_w1::open_pair_event(ev, &nsk) {
Ok((PairKind::Request, card)) => Some(card),
_ => None,
})
.ok_or_else(|| anyhow!("no pair-request from {npub} found on {relay}"))?;
crate::agent_card::verify_agent_card(&card)
.map_err(|e| anyhow!("peer card signature invalid: {e}"))?;
let id_pubkey = card_identity_pubkey(&card)?;
let bound = crate::nostr_key::card_nostr_binding(&card, &id_pubkey)
.map_err(|e| anyhow!("peer card nostr binding invalid: {e}"))?
.ok_or_else(|| anyhow!("peer card carries no nostr binding — cannot tie it to {npub}"))?;
if bound != peer {
bail!("peer card's bound npub does not match the sender — refusing to pin");
}
let peer_did = card
.get("did")
.and_then(Value::as_str)
.unwrap_or("<unknown>")
.to_string();
crate::config::update_trust(|t| {
crate::trust::add_agent_card_pin(t, &card, Some("VERIFIED")).map_err(anyhow::Error::msg)
})?;
let peer_handle = card
.get("handle")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| crate::agent_card::display_handle_from_did(&peer_did).to_string());
crate::config::update_relay_state(|rs| {
crate::endpoints::pin_peer_nostr_transport(rs, &peer_handle, npub, relay)?;
crate::endpoints::pin_self_nostr_relay(rs, relay)
})?;
let ack = nip_w1::build_pair_event(PairKind::Ack, &nsk, &peer, &my_card, now_unix())
.map_err(|e| anyhow!("build pair-ack: {e}"))?;
let acked = block_on(async {
let mut ws = NostrWs::connect(relay)
.await
.with_context(|| format!("connect {relay}"))?;
ws.publish(&ack).await.context("publish pair-ack")
})??;
if as_json {
println!(
"{}",
serde_json::to_string(&json!({
"pinned": peer_did, "tier": "VERIFIED", "ack_sent": acked, "npub": npub, "relay": relay,
}))?
);
} else {
println!(
"→ paired with {peer_did}\n pinned VERIFIED; pair-ack {} via {relay}",
if acked {
"sent"
} else {
"NOT accepted by relay"
}
);
println!(
" note: trust is established; routing wire messages over Nostr (vs the trust pin) is a separate step."
);
}
Ok(())
}
fn cmd_pair(npub: &str, relay: &str, as_json: bool) -> Result<()> {
let nsk = require_transport_key()?;
let peer = parse_npub_hex(npub)?;
let card = crate::config::read_agent_card().context("no agent card — run `wire up` first")?;
let ev = nip_w1::build_pair_event(PairKind::Request, &nsk, &peer, &card, now_unix())
.map_err(|e| anyhow!("build pair-request: {e}"))?;
let event_id = ev.id.clone();
let accepted = block_on(async {
let mut ws = NostrWs::connect(relay)
.await
.with_context(|| format!("connect {relay}"))?;
ws.publish(&ev).await.context("publish pair-request")
})??;
crate::config::update_relay_state(|rs| crate::endpoints::pin_self_nostr_relay(rs, relay))?;
if as_json {
println!(
"{}",
serde_json::to_string(&json!({
"sent": accepted, "event_id": event_id, "to_npub": npub, "relay": relay,
}))?
);
} else if accepted {
println!(
"→ pair-request sent to {npub} via {relay}\n event {event_id}\n the peer accepts with `wire nostr accept <your-npub> --relay {relay}`."
);
} else {
bail!("relay {relay} rejected the pair-request (OK=false)");
}
Ok(())
}
fn cmd_fetch(relay: &str, limit: usize, as_json: bool) -> Result<()> {
let nsk = require_transport_key()?;
let my_xonly = crate::nostr_key::xonly_from_secret(&nsk)
.map_err(|e| anyhow!("transport key unusable: {e}"))?;
crate::config::update_relay_state(|rs| crate::endpoints::pin_self_nostr_relay(rs, relay))?;
let filter = Filter {
p_tags: vec![hex::encode(my_xonly)],
kinds: vec![
nip_w1::PAIR_REQUEST_KIND,
nip_w1::PAIR_ACK_KIND,
1, ],
limit: Some(limit),
..Default::default()
};
let events = block_on(async {
let mut ws = NostrWs::connect(relay)
.await
.with_context(|| format!("connect {relay}"))?;
ws.pull(filter).await.context("pull")
})??;
let mut items = Vec::new();
for ev in &events {
let item = match nip_w1::open_pair_event(ev, &nsk) {
Ok((pair, card)) => json!({
"type": match pair { PairKind::Request => "pair_request", PairKind::Ack => "pair_ack" },
"from_did": card.get("did").and_then(Value::as_str),
"from_handle": card.get("handle").and_then(Value::as_str),
"event_id": ev.id,
}),
Err(nip_w1::NipW1Error::NotPairing) => match nostr_event::verify_and_decode(ev) {
Ok(wire) => json!({
"type": "message",
"from_did": wire.get("from").and_then(Value::as_str),
"kind": wire.get("kind"),
"event_id": ev.id,
}),
Err(_) => continue,
},
Err(_) => continue,
};
items.push(item);
}
if as_json {
println!(
"{}",
serde_json::to_string(&json!({ "relay": relay, "items": items }))?
);
} else {
println!("→ {} event(s) addressed to you on {relay}:", items.len());
for it in &items {
let t = it.get("type").and_then(Value::as_str).unwrap_or("?");
let who = it
.get("from_handle")
.and_then(Value::as_str)
.or_else(|| it.get("from_did").and_then(Value::as_str))
.unwrap_or("<unknown>");
println!(" [{t}] from {who}");
}
if items.is_empty() {
println!(" (nothing — peers must publish to this relay + p-tag your npub)");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_npub_hex_accepts_64_hex_and_rejects_otherwise() {
let ok = "0".repeat(64);
assert_eq!(parse_npub_hex(&ok).unwrap(), [0u8; 32]);
assert!(parse_npub_hex("00").is_err());
assert!(parse_npub_hex(&"0".repeat(66)).is_err());
assert!(parse_npub_hex(&"z".repeat(64)).is_err());
}
}