gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **Invites are the identity core's, persisted by the server** (IDENTITY-HOST,
//! 2026-10-27; Stage L before it): a pending passkey link survives a restart in
//! `<data root>/identity.json` beside the principal it is for, holds only the
//! secret's hash, and opens nothing once its person is gone. (No invite makes
//! an admin: the core's rule, `gunnar-grpc/tests/invites.rs`.)

use std::sync::Arc;

use gunnar_grpc::{ControlPlane, ServerConfig};
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use nornir_identity::Relay;

fn person(stores: &ServerStores, id: &str, kind: gunnar_core::PrincipalKind) {
    stores
        .auth()
        .add_principal(gunnar_core::Principal {
            id: id.to_owned(),
            name: id.to_owned(),
            kind,
            created: 0,
        })
        .expect("principal");
}

fn relay() -> Relay {
    Relay {
        rp_id: "console.example".into(),
        origin: "https://console.example".into(),
        client: None,
    }
}

/// ★ A link minted before a restart opens its person's ceremony after it; the
/// document on disk holds no secret; and it opens nothing once the person is
/// removed.
#[test]
fn an_invite_survives_a_restart_and_goes_with_its_principal() {
    let dir = tempfile::tempdir().expect("tempdir");
    let secret = {
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        person(&stores, "kim", gunnar_core::PrincipalKind::User);
        let control: Arc<dyn ControlPlane> = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
        let identity =
            gunnar_server::identity::build(dir.path(), control, &ServerConfig::default(), false);
        identity
            .enrolment("kim", "boss", None)
            .expect("kim's link")
            .secret
    };
    let on_disk = std::fs::read_to_string(dir.path().join(gunnar_server::identity::FILE))
        .expect("identity.json");
    assert!(!on_disk.contains(&secret), "a secret on disk");

    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
    let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
    let control: Arc<dyn ControlPlane> = plane.clone();
    let identity =
        gunnar_server::identity::build(dir.path(), control, &ServerConfig::default(), false);
    let opened = identity
        .redeem_start(&secret, "", &relay())
        .expect("the link after a restart");
    assert_eq!(opened.user_id, "kim");

    plane.remove_principal("kim").expect("removed");
    assert!(
        identity.redeem_start(&secret, "", &relay()).is_err(),
        "the link outlived kim"
    );
}