nip05: advertise only the relays the operator chose

relay_hints appended damus/nos.lol unconditionally, so every self-hosted instance
published a NIP-05 document pointing its own users at third-party relays, and a
correspondent would deliver there instead of to the operator's relay. An operator
running a private deployment had no way to stop it.

Now opt-in: NIP05_INTEROP_HINTS (default off) for the built-in interop pair, plus
NIP05_EXTRA_RELAY_HINTS for the operator's own list.
This commit is contained in:
jwinterm
2026-08-01 05:39:24 -04:00
parent ae118edf86
commit 0b410c2b00
2 changed files with 37 additions and 15 deletions
+24 -15
View File
@@ -40,16 +40,16 @@ use crate::AppState;
// ── constants ───────────────────────────────────────────────────────────────── // ── constants ─────────────────────────────────────────────────────────────────
/// Default relay hints advertised in the well-known `relays` map. /// Fallback interop relay hints, used ONLY when the operator has opted in via
/// `NIP05_EXTRA_RELAY_HINTS`.
/// ///
/// Public relays shared with other wallets so cross-wallet delivery works before /// These are third-party relays. Advertising them unconditionally made every
/// any per-user relay list is configured. NIP-05 lets a server advertise relay /// self-hosted instance publish a document pointing its own users at
/// hints per pubkey in the `relays` object; we serve the same default set for /// damus/nos.lol, so a correspondent would deliver there instead of to the
/// every resolved name. /// operator's relay. An operator running a private or air-gapped deployment had
/// /// no way to stop their NIP-05 document recommending someone else's
/// TODO(phase-2): make configurable and serve per-user lists (NIP-65 kind 10002 /// infrastructure. Opt-in only.
/// / NIP-17 inbox relays) when private-message delivery lands. const INTEROP_RELAY_HINTS: &[&str] = &["wss://relay.damus.io", "wss://nos.lol"];
const DEFAULT_RELAYS: &[&str] = &["wss://relay.damus.io", "wss://nos.lol"];
// ── DTOs ────────────────────────────────────────────────────────────────────── // ── DTOs ──────────────────────────────────────────────────────────────────────
@@ -118,18 +118,27 @@ pub async fn well_known_nostr(
} }
/// Relay hints advertised for a resolved pubkey: this instance's own relay (the /// Relay hints advertised for a resolved pubkey: this instance's own relay (the
/// user's DM inbox) FIRST when enabled, then the public interop relays so /// user's DM inbox) first, then any interop relays the OPERATOR opted into.
/// cross-wallet (e.g. Goblin) delivery still works before any per-user list. ///
/// Per-user lists (NIP-65 kind 10002 / NIP-17 inbox relays) remain future work. /// An instance advertises only what its operator chose. Per-user lists (NIP-65
/// kind 10002 / NIP-17 inbox relays) remain future work.
fn relay_hints(config: &crate::config::Config) -> Vec<String> { fn relay_hints(config: &crate::config::Config) -> Vec<String> {
let mut hints = Vec::new(); let mut hints = Vec::new();
let r = &config.messaging.relay; let r = &config.messaging.relay;
if r.enabled && !r.advertised_url.trim().is_empty() { if r.enabled && !r.advertised_url.trim().is_empty() {
hints.push(r.advertised_url.clone()); hints.push(r.advertised_url.clone());
} }
for d in DEFAULT_RELAYS { if config.messaging.nip05_interop_hints {
if !hints.iter().any(|h| h == d) { for d in INTEROP_RELAY_HINTS {
hints.push((*d).to_string()); if !hints.iter().any(|h| h == d) {
hints.push((*d).to_string());
}
}
}
for extra in &config.messaging.nip05_extra_relay_hints {
let extra = extra.trim();
if !extra.is_empty() && !hints.iter().any(|h| h == extra) {
hints.push(extra.to_string());
} }
} }
hints hints
+13
View File
@@ -655,6 +655,15 @@ fn parse_premium_plans(s: &str) -> Vec<PremiumPlan> {
#[derive(Clone)] #[derive(Clone)]
pub struct MessagingConfig { pub struct MessagingConfig {
pub relay: RelayConfig, pub relay: RelayConfig,
/// Advertise the built-in third-party interop relays (damus, nos.lol) in the
/// NIP-05 document. OFF by default: an instance should recommend only what
/// its operator chose. Turn on for a public deployment that wants
/// cross-wallet delivery to work before correspondents publish their own
/// relay lists.
pub nip05_interop_hints: bool,
/// Additional relay URLs to advertise in the NIP-05 document, comma-separated
/// in `NIP05_EXTRA_RELAY_HINTS`. Appended after the instance's own relay.
pub nip05_extra_relay_hints: Vec<String>,
} }
/// Optional first-party Nostr relay. When `enabled`, the operator runs a relay /// Optional first-party Nostr relay. When `enabled`, the operator runs a relay
@@ -927,6 +936,8 @@ impl Config {
retention_days: env_parse("RELAY_RETENTION_DAYS", 30u32)?, retention_days: env_parse("RELAY_RETENTION_DAYS", 30u32)?,
write_allowlist: env_list("RELAY_WRITE_ALLOWLIST_NPUBS"), write_allowlist: env_list("RELAY_WRITE_ALLOWLIST_NPUBS"),
}, },
nip05_interop_hints: env_bool("NIP05_INTEROP_HINTS", false),
nip05_extra_relay_hints: env_list("NIP05_EXTRA_RELAY_HINTS"),
}, },
premium: PremiumConfig { premium: PremiumConfig {
enabled: env_bool("PREMIUM_ENABLED", false), enabled: env_bool("PREMIUM_ENABLED", false),
@@ -1527,6 +1538,8 @@ mod tests {
retention_days: 30, retention_days: 30,
write_allowlist: Vec::new(), write_allowlist: Vec::new(),
}, },
nip05_interop_hints: false,
nip05_extra_relay_hints: Vec::new(),
}, },
premium: PremiumConfig { premium: PremiumConfig {
enabled: false, enabled: false,