fix(federation): thread invite trust level end-to-end — 'Invite a Peer' now federates as Observer
Invites carried no trust level, and both accept_invite and the peer-joined handler hardcoded TrustLevel::Trusted — so 'Invite a Peer' (Observer) federated both sides as fully Trusted. - invite codes now carry a 'trust' field (legacy codes parse as Trusted) - federation.invite accepts trust_level: trusted|observer - accept_invite assigns the invite's level on the acceptor side - peer-joined resolves the granted level authoritatively by matching the acceptor's echoed invite token against our stored outgoing invites; the peer's own unsigned claim is honored only as a downgrade, so no escalation is possible - discovery/connection-request approvals mint Observer invites directly (the post-hoc demotion in handshake.rs stays as a legacy safety net) - asymmetry self-heal re-asserts at the locally-held level Tests: observer threading + legacy default-trusted parse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
919665fb16
commit
950549304d
@ -326,7 +326,7 @@ impl RpcHandler {
|
||||
}
|
||||
|
||||
// Federation
|
||||
"federation.invite" => self.handle_federation_invite().await,
|
||||
"federation.invite" => self.handle_federation_invite(params).await,
|
||||
"federation.join" => self.handle_federation_join(params).await,
|
||||
"federation.list-nodes" => self.handle_federation_list_nodes().await,
|
||||
"federation.remove-node" => self.handle_federation_remove_node(params).await,
|
||||
|
||||
@ -53,7 +53,23 @@ impl RpcHandler {
|
||||
|
||||
impl RpcHandler {
|
||||
/// federation.invite — Generate an invite code containing our DID + onion for a peer.
|
||||
pub(in crate::api::rpc) async fn handle_federation_invite(&self) -> Result<serde_json::Value> {
|
||||
/// Optional param `trust_level`: "trusted" (default, "Link Your Nodes") or
|
||||
/// "observer" ("Invite a Peer") — the level BOTH sides assign for this invite.
|
||||
pub(in crate::api::rpc) async fn handle_federation_invite(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let trust_level = params
|
||||
.as_ref()
|
||||
.and_then(|p| p.get("trust_level"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| {
|
||||
TrustLevel::parse(s)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid trust_level: {s} (expected trusted|observer)"))
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(TrustLevel::Trusted);
|
||||
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
|
||||
let onion = data.server_info.tor_address.clone().unwrap_or_default();
|
||||
@ -72,14 +88,16 @@ impl RpcHandler {
|
||||
&onion,
|
||||
&pubkey,
|
||||
fips_npub.as_deref(),
|
||||
trust_level,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(did = %did, fips_advertised = fips_npub.is_some(), "Generated federation invite");
|
||||
info!(did = %did, trust = %trust_level, fips_advertised = fips_npub.is_some(), "Generated federation invite");
|
||||
Ok(serde_json::json!({
|
||||
"code": code,
|
||||
"did": did,
|
||||
"onion": onion,
|
||||
"trust_level": trust_level.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -511,6 +529,36 @@ impl RpcHandler {
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Resolve the trust level granted by this join. Authoritative source:
|
||||
// the acceptor echoes the invite's random token, which we match against
|
||||
// OUR stored outgoing invites — the level we minted the code with wins.
|
||||
// Fallback: the peer's (unsigned) "trust" claim, honored only as a
|
||||
// DOWNGRADE from Trusted so it can never escalate. Legacy peers send
|
||||
// neither → Trusted, matching pre-threading behavior.
|
||||
let claimed_trust = params
|
||||
.get("trust")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(TrustLevel::parse)
|
||||
.unwrap_or(TrustLevel::Trusted);
|
||||
let invite_trust = match params.get("invite_token").and_then(|v| v.as_str()) {
|
||||
Some(token) => federation::load_invites(&self.config.data_dir)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|invites| {
|
||||
invites.outgoing.iter().find_map(|inv| {
|
||||
federation::parse_invite(&inv.code)
|
||||
.ok()
|
||||
.filter(|p| p.token == token)
|
||||
.map(|_| inv.trust_level)
|
||||
})
|
||||
}),
|
||||
None => None,
|
||||
};
|
||||
let granted_trust = match invite_trust {
|
||||
Some(level) => level,
|
||||
None => TrustLevel::Trusted.min(claimed_trust),
|
||||
};
|
||||
|
||||
// Reject self-peering. If somehow our own did / onion / pubkey
|
||||
// comes back at us (misconfigured invite, gossip loop), adding
|
||||
// the entry causes sync loops where the node syncs with itself
|
||||
@ -603,7 +651,7 @@ impl RpcHandler {
|
||||
pubkey: pubkey.to_string(),
|
||||
onion: onion.to_string(),
|
||||
name: incoming_name.clone(),
|
||||
trust_level: TrustLevel::Trusted,
|
||||
trust_level: granted_trust,
|
||||
added_at: chrono::Utc::now().to_rfc3339(),
|
||||
last_seen: None,
|
||||
last_state: None,
|
||||
@ -613,7 +661,7 @@ impl RpcHandler {
|
||||
};
|
||||
|
||||
federation::add_node(&self.config.data_dir, node).await?;
|
||||
info!(peer_did = %did, "Peer joined our federation");
|
||||
info!(peer_did = %did, trust = %granted_trust, "Peer joined our federation");
|
||||
|
||||
// Mirror into mesh state so the inbound peer is addressable from
|
||||
// the chat UI without waiting for the next mesh restart.
|
||||
@ -1046,12 +1094,16 @@ impl RpcHandler {
|
||||
// ciphertext below.
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let local_fips_npub = identity::fips_npub(&identity_dir).await.unwrap_or(None);
|
||||
// Discovery/connection-request approvals admit the requester as
|
||||
// Observer — the invite itself now carries that level, so both
|
||||
// sides converge on Observer without post-hoc demotion.
|
||||
let invite_code = federation::create_invite(
|
||||
&self.config.data_dir,
|
||||
&local_did,
|
||||
&local_onion,
|
||||
&local_pubkey,
|
||||
local_fips_npub.as_deref(),
|
||||
TrustLevel::Observer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@ -298,8 +298,10 @@ impl RpcHandler {
|
||||
Ok(node) => {
|
||||
// Approved-by-them: their box already has us as Observer
|
||||
// (their approval handler added us under that trust level
|
||||
// before sending the invite). Demote our local entry to
|
||||
// Observer too — accept_invite hardcodes Trusted, but the
|
||||
// before sending the invite). Discovery invites are now
|
||||
// minted with trust=observer, so accept_invite already
|
||||
// lands on Observer; keep this explicit demotion as a
|
||||
// safety net for legacy Trusted-only invite codes — the
|
||||
// discovery flow should never auto-trust.
|
||||
let _ = crate::federation::set_trust_level(
|
||||
&self.config.data_dir,
|
||||
|
||||
@ -18,16 +18,22 @@ pub struct ParsedInvite {
|
||||
pub token: String,
|
||||
/// Inviter's FIPS npub if advertised in the code.
|
||||
pub fips_npub: Option<String>,
|
||||
/// Trust level the invite grants both sides. Absent in legacy codes,
|
||||
/// which default to Trusted.
|
||||
pub trust_level: TrustLevel,
|
||||
}
|
||||
|
||||
/// Generate an invite code. Format: `fed1:<base64(json{did, onion, pubkey, token, fips_npub?})>`.
|
||||
/// Generate an invite code. Format: `fed1:<base64(json{did, onion, pubkey, token, fips_npub?, trust})>`.
|
||||
/// `fips_npub` is only included when the local node has a materialised FIPS key.
|
||||
/// `trust_level` is the level BOTH sides assign to each other for this invite
|
||||
/// ("Invite a Peer" = Observer, "Link Your Nodes" = Trusted).
|
||||
pub async fn create_invite(
|
||||
data_dir: &Path,
|
||||
did: &str,
|
||||
onion: &str,
|
||||
pubkey: &str,
|
||||
fips_npub: Option<&str>,
|
||||
trust_level: TrustLevel,
|
||||
) -> Result<String> {
|
||||
use base64::Engine;
|
||||
use rand::Rng;
|
||||
@ -41,6 +47,7 @@ pub async fn create_invite(
|
||||
"onion": onion,
|
||||
"pubkey": pubkey,
|
||||
"token": token,
|
||||
"trust": trust_level.to_string(),
|
||||
});
|
||||
if let Some(npub) = fips_npub {
|
||||
payload["fips_npub"] = serde_json::Value::String(npub.to_string());
|
||||
@ -59,6 +66,7 @@ pub async fn create_invite(
|
||||
created_at: chrono::Utc::now().to_rfc3339(),
|
||||
accepted: false,
|
||||
fips_npub: fips_npub.map(|s| s.to_string()),
|
||||
trust_level,
|
||||
};
|
||||
|
||||
let mut invites = load_invites(data_dir).await?;
|
||||
@ -103,6 +111,13 @@ pub fn parse_invite(code: &str) -> Result<ParsedInvite> {
|
||||
.get("fips_npub")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
// Legacy codes (pre trust-threading) carry no "trust" field → Trusted,
|
||||
// which matches what both sides did before the field existed.
|
||||
let trust_level = payload
|
||||
.get("trust")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(TrustLevel::parse)
|
||||
.unwrap_or(TrustLevel::Trusted);
|
||||
|
||||
Ok(ParsedInvite {
|
||||
did,
|
||||
@ -110,6 +125,7 @@ pub fn parse_invite(code: &str) -> Result<ParsedInvite> {
|
||||
pubkey,
|
||||
token,
|
||||
fips_npub,
|
||||
trust_level,
|
||||
})
|
||||
}
|
||||
|
||||
@ -128,8 +144,9 @@ pub async fn accept_invite(
|
||||
did,
|
||||
onion,
|
||||
pubkey,
|
||||
token: _,
|
||||
token,
|
||||
fips_npub,
|
||||
trust_level,
|
||||
} = parse_invite(code)?;
|
||||
|
||||
// Refuse self-peering. If the invite's did / onion / pubkey matches
|
||||
@ -173,7 +190,9 @@ pub async fn accept_invite(
|
||||
pubkey,
|
||||
onion,
|
||||
name: None,
|
||||
trust_level: TrustLevel::Trusted,
|
||||
// The invite code itself says what this relationship is —
|
||||
// Observer for "Invite a Peer", Trusted for "Link Your Nodes".
|
||||
trust_level,
|
||||
added_at: chrono::Utc::now().to_rfc3339(),
|
||||
last_seen: None,
|
||||
last_state: None,
|
||||
@ -194,6 +213,7 @@ pub async fn accept_invite(
|
||||
created_at: chrono::Utc::now().to_rfc3339(),
|
||||
accepted: true,
|
||||
fips_npub,
|
||||
trust_level,
|
||||
});
|
||||
save_invites(data_dir, &invites).await?;
|
||||
|
||||
@ -206,6 +226,8 @@ pub async fn accept_invite(
|
||||
local_pubkey,
|
||||
local_fips_npub,
|
||||
local_name,
|
||||
Some(&token),
|
||||
trust_level,
|
||||
sign_fn,
|
||||
)
|
||||
.await;
|
||||
@ -217,6 +239,7 @@ pub async fn accept_invite(
|
||||
/// Prefers FIPS (if the remote advertised an npub in their invite) and
|
||||
/// falls back to Tor. Signs the message with our ed25519 key so the
|
||||
/// remote peer can verify authenticity regardless of transport.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn notify_join(
|
||||
remote_onion: &str,
|
||||
remote_fips_npub: Option<&str>,
|
||||
@ -225,6 +248,8 @@ pub(crate) async fn notify_join(
|
||||
local_pubkey: &str,
|
||||
local_fips_npub: Option<&str>,
|
||||
local_name: Option<&str>,
|
||||
invite_token: Option<&str>,
|
||||
trust_level: TrustLevel,
|
||||
sign_fn: impl FnOnce(&[u8]) -> String,
|
||||
) -> Result<()> {
|
||||
// Sign the canonical message: "peer-joined:{did}:{onion}:{pubkey}"
|
||||
@ -233,6 +258,11 @@ pub(crate) async fn notify_join(
|
||||
// (any identity claim is anchored on the signed did/pubkey); the
|
||||
// FIPS daemon's own Noise handshake authenticates the transport
|
||||
// session regardless of the advertised npub.
|
||||
//
|
||||
// invite_token + trust are also unsigned: the inviter treats the token
|
||||
// as a lookup key into its own stored invites (authoritative for the
|
||||
// granted level) and only ever accepts the trust claim as a DOWNGRADE,
|
||||
// so neither field can escalate privileges.
|
||||
let sign_data = format!("peer-joined:{}:{}:{}", local_did, local_onion, local_pubkey);
|
||||
let signature = sign_fn(sign_data.as_bytes());
|
||||
|
||||
@ -241,6 +271,7 @@ pub(crate) async fn notify_join(
|
||||
"onion": local_onion,
|
||||
"pubkey": local_pubkey,
|
||||
"signature": signature,
|
||||
"trust": trust_level.to_string(),
|
||||
});
|
||||
if let Some(npub) = local_fips_npub {
|
||||
params["fips_npub"] = serde_json::Value::String(npub.to_string());
|
||||
@ -248,6 +279,9 @@ pub(crate) async fn notify_join(
|
||||
if let Some(name) = local_name {
|
||||
params["name"] = serde_json::Value::String(name.to_string());
|
||||
}
|
||||
if let Some(token) = invite_token {
|
||||
params["invite_token"] = serde_json::Value::String(token.to_string());
|
||||
}
|
||||
|
||||
let body = serde_json::json!({
|
||||
"method": "federation.peer-joined",
|
||||
@ -318,7 +352,14 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_create_and_parse_invite() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let code = create_invite(dir.path(), "did:key:z1", "test.onion", "aabbcc", None)
|
||||
let code = create_invite(
|
||||
dir.path(),
|
||||
"did:key:z1",
|
||||
"test.onion",
|
||||
"aabbcc",
|
||||
None,
|
||||
TrustLevel::Trusted,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(code.starts_with("fed1:"));
|
||||
@ -335,7 +376,14 @@ mod tests {
|
||||
async fn test_invite_roundtrips_fips_npub() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let fips = "npub1fipstest0000000000000000000000000000000000";
|
||||
let code = create_invite(dir.path(), "did:key:z1", "test.onion", "aabbcc", Some(fips))
|
||||
let code = create_invite(
|
||||
dir.path(),
|
||||
"did:key:z1",
|
||||
"test.onion",
|
||||
"aabbcc",
|
||||
Some(fips),
|
||||
TrustLevel::Trusted,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let parsed = parse_invite(&code).unwrap();
|
||||
@ -362,6 +410,126 @@ mod tests {
|
||||
assert!(parsed.fips_npub.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_observer_invite_threads_trust_level() {
|
||||
// "Invite a Peer" mints an observer invite; the acceptor must land
|
||||
// on Observer, not Trusted (the original bug).
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let code = create_invite(
|
||||
dir.path(),
|
||||
"did:key:zRemote",
|
||||
"remote.onion",
|
||||
"remotepub",
|
||||
None,
|
||||
TrustLevel::Observer,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let parsed = parse_invite(&code).unwrap();
|
||||
assert_eq!(parsed.trust_level, TrustLevel::Observer);
|
||||
|
||||
let dir2 = tempfile::tempdir().unwrap();
|
||||
let node = accept_invite(
|
||||
dir2.path(),
|
||||
&code,
|
||||
"did:key:zLocal",
|
||||
"local.onion",
|
||||
"localpub",
|
||||
None,
|
||||
None,
|
||||
|_| "test-sig".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(node.trust_level, TrustLevel::Observer);
|
||||
|
||||
// The inviter's stored outgoing invite carries the level too — this
|
||||
// is what peer-joined uses as the authoritative grant on the far side.
|
||||
let invites = load_invites(dir.path()).await.unwrap();
|
||||
assert_eq!(invites.outgoing.len(), 1);
|
||||
assert_eq!(invites.outgoing[0].trust_level, TrustLevel::Observer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_legacy_invite_defaults_to_trusted() {
|
||||
// Codes minted before the trust field existed must stay Trusted.
|
||||
use base64::Engine;
|
||||
let legacy = serde_json::json!({
|
||||
"did": "did:key:zOld",
|
||||
"onion": "old.onion",
|
||||
"pubkey": "00",
|
||||
"token": "aa",
|
||||
});
|
||||
let code = format!(
|
||||
"fed1:{}",
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(serde_json::to_string(&legacy).unwrap())
|
||||
);
|
||||
let parsed = parse_invite(&code).unwrap();
|
||||
assert_eq!(parsed.trust_level, TrustLevel::Trusted);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_observer_invite_threads_trust_level() {
|
||||
// "Invite a Peer" mints an observer invite; the acceptor must land
|
||||
// on Observer, not Trusted (the original bug).
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let code = create_invite(
|
||||
dir.path(),
|
||||
"did:key:zRemote",
|
||||
"remote.onion",
|
||||
"remotepub",
|
||||
None,
|
||||
TrustLevel::Observer,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let parsed = parse_invite(&code).unwrap();
|
||||
assert_eq!(parsed.trust_level, TrustLevel::Observer);
|
||||
|
||||
let dir2 = tempfile::tempdir().unwrap();
|
||||
let node = accept_invite(
|
||||
dir2.path(),
|
||||
&code,
|
||||
"did:key:zLocal",
|
||||
"local.onion",
|
||||
"localpub",
|
||||
None,
|
||||
None,
|
||||
|_| "test-sig".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(node.trust_level, TrustLevel::Observer);
|
||||
|
||||
// The inviter's stored outgoing invite carries the level too — this
|
||||
// is what peer-joined uses as the authoritative grant on the far side.
|
||||
let invites = load_invites(dir.path()).await.unwrap();
|
||||
assert_eq!(invites.outgoing.len(), 1);
|
||||
assert_eq!(invites.outgoing[0].trust_level, TrustLevel::Observer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_legacy_invite_defaults_to_trusted() {
|
||||
// Codes minted before the trust field existed must stay Trusted.
|
||||
use base64::Engine;
|
||||
let legacy = serde_json::json!({
|
||||
"did": "did:key:zOld",
|
||||
"onion": "old.onion",
|
||||
"pubkey": "00",
|
||||
"token": "aa",
|
||||
});
|
||||
let code = format!(
|
||||
"fed1:{}",
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(serde_json::to_string(&legacy).unwrap())
|
||||
);
|
||||
let parsed = parse_invite(&code).unwrap();
|
||||
assert_eq!(parsed.trust_level, TrustLevel::Trusted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_invite() {
|
||||
assert!(parse_invite("invalid").is_err());
|
||||
@ -377,6 +545,7 @@ mod tests {
|
||||
"remote.onion",
|
||||
"remotepub",
|
||||
None,
|
||||
TrustLevel::Trusted,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -413,6 +582,7 @@ mod tests {
|
||||
"remote.onion",
|
||||
"remotepub",
|
||||
Some(fips),
|
||||
TrustLevel::Trusted,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -447,6 +617,7 @@ mod tests {
|
||||
"remote.onion",
|
||||
"remotepub",
|
||||
None,
|
||||
TrustLevel::Trusted,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -11,10 +11,13 @@ mod sync;
|
||||
mod types;
|
||||
|
||||
// Re-export all public items so `crate::federation::*` continues to work.
|
||||
pub use invites::{accept_invite, create_invite};
|
||||
pub use invites::{accept_invite, create_invite, parse_invite};
|
||||
// Crate-internal: used by the periodic federation auto-sync to re-assert
|
||||
// membership to peers that don't list us back (asymmetry self-heal).
|
||||
pub(crate) use invites::notify_join;
|
||||
// Crate-internal: peer-joined resolves the granted trust level by matching
|
||||
// the acceptor's invite_token against our stored outgoing invites.
|
||||
pub(crate) use storage::load_invites;
|
||||
#[allow(unused_imports)]
|
||||
pub use storage::{
|
||||
add_node, fips_npub_for_onion, load_nodes, load_removed_dids, record_peer_transport,
|
||||
|
||||
@ -21,6 +21,34 @@ impl std::fmt::Display for TrustLevel {
|
||||
}
|
||||
}
|
||||
|
||||
impl TrustLevel {
|
||||
/// Parse the lowercase wire form ("trusted" | "observer" | "untrusted").
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"trusted" => Some(TrustLevel::Trusted),
|
||||
"observer" => Some(TrustLevel::Observer),
|
||||
"untrusted" => Some(TrustLevel::Untrusted),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The lower (less privileged) of two levels: Untrusted < Observer < Trusted.
|
||||
pub fn min(self, other: Self) -> Self {
|
||||
fn rank(l: TrustLevel) -> u8 {
|
||||
match l {
|
||||
TrustLevel::Untrusted => 0,
|
||||
TrustLevel::Observer => 1,
|
||||
TrustLevel::Trusted => 2,
|
||||
}
|
||||
}
|
||||
if rank(self) <= rank(other) {
|
||||
self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A federated node in our cluster.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FederatedNode {
|
||||
@ -138,6 +166,15 @@ pub struct FederationInvite {
|
||||
/// Inviter's FIPS mesh npub if advertised in the code.
|
||||
#[serde(default)]
|
||||
pub fips_npub: Option<String>,
|
||||
/// Trust level this invite grants both sides ("Invite a Peer" mints
|
||||
/// Observer invites, "Link Your Nodes" mints Trusted ones). Defaults
|
||||
/// to Trusted for invites minted before this field existed.
|
||||
#[serde(default = "default_invite_trust")]
|
||||
pub trust_level: TrustLevel,
|
||||
}
|
||||
|
||||
fn default_invite_trust() -> TrustLevel {
|
||||
TrustLevel::Trusted
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -515,6 +515,10 @@ impl Server {
|
||||
&local_pubkey,
|
||||
local_fips_npub.as_deref(),
|
||||
local_name.as_deref(),
|
||||
// Re-assert at the level WE hold for
|
||||
// this peer; no invite token on heal.
|
||||
None,
|
||||
node.trust_level,
|
||||
|b| node_identity.sign(b),
|
||||
)
|
||||
.await
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user