From 23659dfd5aa1ffb967c023d21a2a527daece7c36 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Tue, 4 Aug 2026 06:05:17 -0400 Subject: [PATCH] capabilities: nip05_domain must keep the handle domain users already have Deploying the previous version showed nip05_domain = api.smirk.cash, but handles are name@smirk.cash: that is what v0.2 used, what the wallet derived by stripping 'api.' client-side, and what the kind-0 profiles already on relays carry. Since the wallet now PREFERS this field, shipping it would have silently re-rendered every handle as @api.smirk.cash and disagreed with the published profiles. Caught by checking the deployed output rather than assuming the change was inert. Default is now PUBLIC_API_URL's host with a leading 'api.' removed, preserving the established behaviour, and NIP05_DOMAIN overrides it for operators whose well-known lives elsewhere. Still explicit where it matters; the default just stops being a regression. --- src/api/capabilities.rs | 31 +++++++++++++++++++++---------- src/config.rs | 7 +++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/api/capabilities.rs b/src/api/capabilities.rs index 79dfd3e..c986e4b 100644 --- a/src/api/capabilities.rs +++ b/src/api/capabilities.rs @@ -249,16 +249,27 @@ pub fn effective_capabilities(config: &Config) -> CapabilitiesResponse { enabled: on, network: Some(net.to_string()), }; - // The handle domain is the HOST of the operator's public API URL. No - // `api.` stripping here: if the operator wants handles at the bare domain, - // they say so by setting PUBLIC_API_URL to the host that actually serves - // /.well-known/nostr.json. Guessing is what produced unverifiable handles. - let nip05_domain = config - .identity - .public_api_url - .as_deref() - .and_then(|u| url::Url::parse(u).ok()) - .and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase())); + // The handle domain. `NIP05_DOMAIN` wins when set; otherwise derive it from + // PUBLIC_API_URL's host with a leading `api.` removed. + // + // The stripping is deliberate and load-bearing: handles have been + // `name@smirk.cash` (not `name@api.smirk.cash`) since v0.2, the wallet + // stripped `api.` client-side for exactly that reason, and published kind-0 + // profiles already carry the bare form. Advertising the raw API host would + // silently re-render every existing handle and disagree with what is already + // on relays. + // + // It is still only a default. An operator whose well-known lives somewhere + // else sets NIP05_DOMAIN explicitly rather than being second-guessed. + let nip05_domain = config.identity.nip05_domain.clone().or_else(|| { + config + .identity + .public_api_url + .as_deref() + .and_then(|u| url::Url::parse(u).ok()) + .and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase())) + .map(|h| h.strip_prefix("api.").unwrap_or(&h).to_string()) + }); CapabilitiesResponse { version: env!("CARGO_PKG_VERSION").to_string(), contract_version: CAPABILITIES_CONTRACT_VERSION, diff --git a/src/config.rs b/src/config.rs index e67dd5f..e74f8e0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -215,6 +215,11 @@ pub struct IdentityConfig { /// Required when Nostr identity is enabled: it is the canonical value the /// NIP-98 `u` tag is verified against — never the request `Host` header. pub public_api_url: Option, + /// The domain handles live at (`name@`), advertised in + /// `/capabilities`. Defaults to `public_api_url`'s host with a leading + /// `api.` removed, which is what the wallet used to derive client-side. + /// Set explicitly when your `/.well-known/nostr.json` lives elsewhere. + pub nip05_domain: Option, } /// HMAC peppers and salts. Fail-closed: required and length-checked. These make @@ -756,6 +761,7 @@ impl Config { }, identity: IdentityConfig { public_api_url: env_opt("PUBLIC_API_URL"), + nip05_domain: env_opt("NIP05_DOMAIN"), }, secrets: SecretConfig { seed_fingerprint_pepper: env_or("SEED_FINGERPRINT_PEPPER", ""), @@ -1442,6 +1448,7 @@ mod tests { }, identity: IdentityConfig { public_api_url: Some("https://backend.example.org/api/v1".into()), + nip05_domain: None, }, secrets: SecretConfig { seed_fingerprint_pepper: "p".repeat(32),