iris-chat 0.1.61

Iris Chat command line client and shared encrypted chat core
Documentation
use fips_core::{config::PeerConfig, PeerIdentity};

pub(super) fn webrtc_enabled(eligible: bool, configured: Option<&str>) -> bool {
    eligible
        && !configured.is_some_and(|value| {
            matches!(value.trim().to_ascii_lowercase().as_str(), "false" | "0")
        })
}

pub(super) fn pubsub_policy_options() -> nostr_pubsub_fips::FipsPubsubPolicyOptions {
    policy_options(
        &std::env::var_os("IRIS_CHAT_FIPS_TRUSTED_RATERS")
            .unwrap_or_default()
            .to_string_lossy(),
    )
}

fn policy_options(raters: &str) -> nostr_pubsub_fips::FipsPubsubPolicyOptions {
    let mut options = nostr_pubsub_fips::FipsPubsubPolicyOptions::default();
    // The shared adapter validates public keys and bounds this explicit list.
    options.reputation.trusted_raters = raters
        .split([',', ';'])
        .map(str::trim)
        .filter(|key| !key.is_empty())
        .map(str::to_owned)
        .collect();
    options
}

pub(super) fn configured_peer_hints() -> Result<(Vec<PeerConfig>, Vec<PeerIdentity>), String> {
    parse_peer_hints(
        &std::env::var("IRIS_CHAT_FIPS_STATIC_PEERS").unwrap_or_default(),
        &std::env::var("IRIS_CHAT_FIPS_ROUTED_PEERS").unwrap_or_default(),
    )
}

fn parse_peer_hints(
    direct: &str,
    routed: &str,
) -> Result<(Vec<PeerConfig>, Vec<PeerIdentity>), String> {
    let mut peers = Vec::new();
    for entry in direct
        .split([',', ';'])
        .map(str::trim)
        .filter(|s| !s.is_empty())
    {
        let (npub, address) = entry.split_once('=').ok_or("expected npub=udp:address")?;
        let identity = PeerIdentity::from_npub(npub.trim()).map_err(|error| error.to_string())?;
        let address = address
            .trim()
            .strip_prefix("udp:")
            .ok_or("expected udp:address")?;
        let address = address
            .parse::<std::net::SocketAddr>()
            .map_err(|error| error.to_string())?;
        peers.push(PeerConfig::new(identity.npub(), "udp", address.to_string()));
    }
    let mut routed_peers = Vec::new();
    for npub in routed
        .split([',', ';'])
        .map(str::trim)
        .filter(|s| !s.is_empty())
    {
        let peer = PeerIdentity::from_npub(npub).map_err(|error| error.to_string())?;
        if !routed_peers.contains(&peer) {
            routed_peers.push(peer);
        }
    }
    Ok((peers, routed_peers))
}

pub(super) fn valid_device_sync_chat_id(chat_id: &str) -> bool {
    chat_id
        .strip_prefix("group:")
        .is_some_and(|group_id| !group_id.is_empty() && group_id.len() <= 128)
        || nostr::PublicKey::from_hex(chat_id).is_ok()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn explicit_webrtc_opt_out_preserves_default_discovery() {
        for configured in [None, Some("true"), Some("1")] {
            assert!(webrtc_enabled(true, configured));
            assert!(!webrtc_enabled(false, configured));
        }
        for configured in [Some("false"), Some(" FALSE "), Some("0")] {
            assert!(!webrtc_enabled(true, configured));
            assert!(!webrtc_enabled(false, configured));
        }
    }

    #[test]
    fn machine_trust_entrypoints_are_explicit_and_empty_by_default() {
        assert!(policy_options("").reputation.trusted_raters.is_empty());
        let public_key = nostr::Keys::generate().public_key().to_hex();
        assert_eq!(
            policy_options(&format!(" {public_key}, ;{public_key} "))
                .reputation
                .trusted_raters,
            std::collections::BTreeSet::from([public_key]),
        );
        assert!(
            policy_options("invalid-public-key")
                .reputation
                .trusted_raters
                .contains("invalid-public-key"),
            "invalid explicit configuration must reach the adapter's validator"
        );
    }

    #[test]
    fn routed_identities_do_not_become_direct_addresses() {
        use nostr::ToBech32;
        let npub = nostr::Keys::generate().public_key().to_bech32().unwrap();
        let (direct, routed) = parse_peer_hints("", &format!("{npub},{npub}")).unwrap();
        assert!(direct.is_empty());
        assert_eq!(routed.len(), 1);
        assert_eq!(routed[0].npub(), npub);
        let (direct, routed) = parse_peer_hints(&format!("{npub}=udp:127.0.0.1:7000"), "").unwrap();
        assert_eq!(direct.len(), 1);
        assert!(routed.is_empty());
    }

    #[test]
    fn invalid_hints_fail_instead_of_disabling_explicit_connectivity() {
        assert!(parse_peer_hints("broken", "").is_err());
        assert!(parse_peer_hints("", "broken").is_err());
    }
}