iris-chat 0.1.51

Iris Chat command line client and shared encrypted chat core
Documentation
fn local_roster_artifact(core: &AppCore) -> Event {
    core.build_local_identity_artifacts()
        .1
        .into_iter()
        .map(|(_, event)| event)
        .find(is_app_keys_event)
        .expect("published roster")
}

#[test]
fn unchanged_local_roster_republication_keeps_one_durable_signed_head() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let directory = tempfile::TempDir::new().unwrap();
    let data_dir = directory.path().to_string_lossy().to_string();
    let mut core = logged_in_test_core_at_data_dir(&owner, &device, data_dir.clone());
    let known = known_app_keys_from_ndr(owner.public_key(), &AppKeys::new(vec![
        DeviceEntry::new(device.public_key(), unix_now().get()),
    ]), unix_now().get());
    core.app_keys.insert(owner.public_key().to_hex(), known.clone());
    let first = local_roster_artifact(&core);
    assert_eq!(local_roster_artifact(&core), first, "unchanged reads reuse the signed roster");
    core.handle_action(AppAction::CreateChat { peer_input: Keys::generate().public_key().to_hex() });
    core.handle_action(AppAction::CreateChat { peer_input: owner.public_key().to_hex() });
    core.send_message(&owner.public_key().to_hex(), "an old note", None);
    core.publish_local_app_keys();
    core.publish_local_app_keys_snapshot_only("test_roster_republish");
    assert_eq!(local_roster_artifact(&core), first, "chat activity does not create a conflicting head");
    for pending in core.pending_relay_publishes.values().filter(|pending| pending.label == "app-keys") {
        let event: Event = serde_json::from_str(&pending.event_json).unwrap();
        assert_eq!(event.id, first.id);
    }
    core.persist_best_effort_inner();
    drop(core);
    let mut restarted = logged_in_test_core_at_data_dir(&owner, &device, data_dir);
    restarted.app_keys.insert(owner.public_key().to_hex(), known);
    assert_eq!(local_roster_artifact(&restarted), first, "restarting retains the exact signed head");
}

#[test]
fn changed_local_roster_requires_new_revision_and_reuses_imported_signed_head() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let sibling = Keys::generate();
    let core = logged_in_test_core("signed-roster-revision", &owner, &device);
    let now = unix_now().get();
    let mut roster = AppKeys::new(vec![DeviceEntry::new(device.public_key(), now)]);
    let first = core.signed_local_app_keys_snapshot(&roster, now, &owner).unwrap();
    roster.add_device(DeviceEntry::new(sibling.public_key(), now));
    assert!(core.signed_local_app_keys_snapshot(&roster, now, &owner).is_err());
    let next = core.signed_local_app_keys_snapshot(&roster, now + 1, &owner).unwrap();
    assert_ne!(next.id, first.id);
    assert_eq!(core.signed_local_app_keys_snapshot(&roster, now + 1, &owner).unwrap(), next);
    assert!(core.signed_local_app_keys_snapshot(&roster, now, &owner).is_err());
    let mut core = core;
    let imported = super::account_signer::canonical_signer_roster(&roster, owner.public_key(), now + 2)
        .sign_with_keys(&owner).unwrap();
    core.cache_local_fips_identity(&imported);
    assert_eq!(core.signed_local_app_keys_snapshot(&roster, now + 2, &owner).unwrap(), imported);
}

#[test]
fn nostrconnect_roster_repair_refuses_real_conflicts_and_replays_one_exact_head() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let client = Keys::generate();
    let now = unix_now().get();
    let roster = AppKeys::new(vec![DeviceEntry::new(device.public_key(), now - 20)]);
    let first = roster.get_event_at(owner.public_key(), now - 1).sign_with_keys(&owner).unwrap();
    let duplicate = roster.get_event_at(owner.public_key(), now - 1).sign_with_keys(&owner).unwrap();
    let mut core = logged_in_test_core("roster-repair", &owner, &device);
    core.app_keys.insert(owner.public_key().to_hex(), known_app_keys_from_ndr(owner.public_key(), &roster, now - 1));
    let relay = crate::local_relay::TestRelay::start();
    let uri = super::remote_signer_uri::client_connection_uri(&client,
        &[RelayUrl::parse(relay.url()).unwrap()], "repair-challenge");
    core.start_device_link_signer(&uri, false);
    let token = core.pending_device_link_signer.as_ref().unwrap().token.clone();
    let heads = vec![first.clone(), duplicate.clone()];
    let repaired = core.prepare_device_link_roster_repair(&token, &heads).unwrap();
    assert!(repaired.created_at > first.created_at);
    let template = heads.iter().min_by_key(|event| event.id).unwrap();
    assert_eq!(repaired.tags, template.tags);
    assert_eq!(repaired.content, template.content);
    assert_eq!(core.prepare_device_link_roster_repair(&token, &heads).unwrap(), repaired);
    for change in ["membership", "join", "extra", "index", "subject", "duplicate_subject", "content"] {
        let mut tags = duplicate.tags.clone().to_vec();
        let mut content = String::new();
        match change {
            "membership" => tags.push(nostr::Tag::parse(["device".into(), Keys::generate().public_key().to_hex(), now.to_string()]).unwrap()),
            "join" => {
                tags.retain(|tag| tag.as_slice().first().is_none_or(|key| key != "device"));
                tags.push(nostr::Tag::parse(["device".into(), device.public_key().to_hex(), now.to_string()]).unwrap());
            }
            "extra" => tags.push(nostr::Tag::parse(["extra", "value"]).unwrap()),
            "index" => tags.push(nostr::Tag::public_key(Keys::generate().public_key())),
            "subject" => {
                tags.retain(|tag| tag.as_slice().first().is_none_or(|key| key != "i"));
                tags.push(nostr::Tag::parse(["i", "not-the-profile", "subject"]).unwrap());
            }
            "duplicate_subject" => tags.push(nostr::Tag::parse(["d", &uuid::Uuid::new_v4().to_string()]).unwrap()),
            "content" => content = "unexpected".into(),
            _ => unreachable!(),
        }
        let changed = UnsignedEvent::new(owner.public_key(), duplicate.created_at, duplicate.kind, tags, content)
            .sign_with_keys(&owner).unwrap();
        assert!(core.prepare_device_link_roster_repair(&token, &[first.clone(), changed]).is_err(), "accepted {change}");
    }
    core.app_keys.get_mut(&owner.public_key().to_hex()).unwrap().created_at_secs = now + 1;
    assert!(core.prepare_device_link_roster_repair(&token, &heads).is_err(), "local edits made during network preparation invalidate cached repair");
    core.stop_device_link_signer();
    assert!(core.prepare_device_link_roster_repair(&token, &heads).is_err());
}

#[test]
fn nostrconnect_roster_repair_never_republishes_obsolete_private_labels() {
    for (wrapped, legacy_count) in [(false, 40), (true, 40), (false, 75), (true, 75)] {
        let owner = Keys::generate();
        let device = Keys::generate();
        let client = Keys::generate();
        let now = unix_now().get();
        let mut devices = vec![DeviceEntry::new(device.public_key(), now - 20)];
        devices.extend((0..3).map(|_| DeviceEntry::new(Keys::generate().public_key(), now - 20)));
        let roster = AppKeys::new(devices);
        let mut private_tag = vec![nostr_double_ratchet::APP_KEYS_ENCRYPTED_DEVICE_LABELS_FACT, "old-private-ciphertext"];
        if wrapped {
            private_tag.insert(0, "f");
        }
        let heads = (0..75).map(|index| {
            let mut unsigned = roster.get_event_at(owner.public_key(), now - 1);
            if index < legacy_count {
                let mut tag = private_tag.iter().map(|value| value.to_string()).collect::<Vec<_>>();
                *tag.last_mut().unwrap() = format!("old-private-ciphertext-{index}");
                unsigned.tags.push(nostr::Tag::parse(tag).unwrap());
            }
            UnsignedEvent::new(owner.public_key(), unsigned.created_at, unsigned.kind,
                unsigned.tags.to_vec(), unsigned.content).sign_with_keys(&owner).unwrap()
        }).collect::<Vec<_>>();
        let mut core = logged_in_test_core("roster-repair-private-labels", &owner, &device);
        core.app_keys.insert(owner.public_key().to_hex(), known_app_keys_from_ndr(owner.public_key(), &roster, now - 1));
        let relay = crate::local_relay::TestRelay::start();
        let uri = super::remote_signer_uri::client_connection_uri(&client,
            &[RelayUrl::parse(relay.url()).unwrap()], "repair-challenge");
        core.start_device_link_signer(&uri, false);
        let token = core.pending_device_link_signer.as_ref().unwrap().token.clone();
        let repaired = core.prepare_device_link_roster_repair(&token, &heads)
            .expect("equivalent device lists with retired labels must remain linkable");
        assert!(repaired.verify().is_ok());
        assert!(repaired.created_at > heads[0].created_at);
        let members = |keys: &AppKeys| keys.get_all_devices().into_iter()
            .map(|entry| (entry.identity_pubkey, entry.created_at)).collect::<BTreeMap<_, _>>();
        assert_eq!(members(&AppKeys::from_event(&repaired).unwrap()), members(&roster));
        assert!(!serde_json::to_string(&repaired).unwrap().contains("encrypted_device_labels"));
        assert!(!serde_json::to_string(&repaired).unwrap().contains("old-private-ciphertext"));
        assert_eq!(core.prepare_device_link_roster_repair(&token, &heads).unwrap(), repaired);
    }
}

#[test]
fn device_link_diagnostics_survive_background_receipt_traffic() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let mut core = logged_in_test_core("link-diagnostics", &owner, &device);
    core.debug_log.clear();
    core.push_debug_log("device_link.approval", "checking_device_list");
    core.push_debug_log("device_link.approval", "device_list_conflict");
    for index in 0..512 {
        core.push_debug_log("fips_nearby.receipt", format!("background receipt {index}"));
    }
    let snapshot = core.build_runtime_debug_snapshot();
    assert!(snapshot.recent_log.iter().any(|entry| entry.detail == "device_list_conflict"));
    assert!(snapshot.recent_log.len() <= MAX_DEBUG_LOG_ENTRIES);
    for _ in 0..512 {
        core.push_debug_log("device_link.approval", "connecting");
    }
    assert!(core.debug_log.len() <= MAX_DEBUG_LOG_ENTRIES);
}