v0.0.21: WZP integration groundwork — CallSignal + token validation

WZP-FC-1: CallSignal WireMessage variant
- CallSignalType enum: Offer, Answer, IceCandidate, Hangup, Reject, Ringing, Busy
- Routed through existing E2E encrypted channels
- Server dedup handles new variant
- TUI shows "📞 Call signal: Offer" etc
- CLI recv prints call signals

WZP-FC-4: Token validation endpoint
- POST /v1/auth/validate { "token": "..." }
- Returns: { "valid": true, "fingerprint": "...", "alias": "..." }
- WZP relay calls this to verify featherChat bearer tokens
- Resolves alias alongside fingerprint

These two unblock WZP integration tasks WZP-S-2 (accept FC tokens)
and WZP-S-3 (signaling bridge mode).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-28 09:13:23 +04:00
parent 65f639052e
commit 064a730b42
8 changed files with 97 additions and 8 deletions

10
warzone/Cargo.lock generated
View File

@@ -2789,7 +2789,7 @@ dependencies = [
[[package]]
name = "warzone-client"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"argon2",
@@ -2822,7 +2822,7 @@ dependencies = [
[[package]]
name = "warzone-mule"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"clap",
@@ -2831,7 +2831,7 @@ dependencies = [
[[package]]
name = "warzone-protocol"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"base64",
"bincode",
@@ -2856,7 +2856,7 @@ dependencies = [
[[package]]
name = "warzone-server"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"axum",
@@ -2883,7 +2883,7 @@ dependencies = [
[[package]]
name = "warzone-wasm"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"base64",
"bincode",

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "0.0.20"
version = "0.0.21"
edition = "2021"
license = "MIT"
rust-version = "1.75"

View File

@@ -129,6 +129,9 @@ pub async fn run(server_url: &str, identity: &IdentityKeyPair) -> Result<()> {
Ok(WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. }) => {
println!(" [sender key] received key from {} for #{}", sender_fingerprint, group_name);
}
Ok(WireMessage::CallSignal { sender_fingerprint, signal_type, target, .. }) => {
println!(" [call] {:?} from {}{}", signal_type, sender_fingerprint, target);
}
Err(e) => {
eprintln!(" failed to deserialize message: {}", e);
}

View File

@@ -1542,6 +1542,22 @@ fn process_wire_message(
message_id: None,
});
}
WireMessage::CallSignal {
id: _,
sender_fingerprint,
signal_type,
payload: _,
target: _,
} => {
let type_str = format!("{:?}", signal_type);
messages.lock().unwrap().push(ChatLine {
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
text: format!("📞 Call signal: {}", type_str),
is_system: false,
is_self: false,
message_id: None,
});
}
}
}

View File

@@ -101,4 +101,34 @@ pub enum WireMessage {
chain_key: [u8; 32],
generation: u32,
},
/// Call signaling: SDP offers/answers, ICE candidates, call control.
/// Routed through featherChat's E2E encrypted channel for WarzonePhone integration.
CallSignal {
id: String,
sender_fingerprint: String,
signal_type: CallSignalType,
/// SDP offer/answer body, ICE candidate, or empty for hangup/reject.
payload: String,
/// Target peer (for 1:1) or group/room name (for group calls).
target: String,
},
}
/// Call signaling types for WarzonePhone integration.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CallSignalType {
/// Initiate a call (contains SDP offer or WZP connection params).
Offer,
/// Accept a call (contains SDP answer or WZP connection params).
Answer,
/// ICE candidate for NAT traversal.
IceCandidate,
/// Hang up / end call.
Hangup,
/// Reject incoming call.
Reject,
/// Call is ringing on the other side.
Ringing,
/// Peer is busy.
Busy,
}

View File

@@ -32,6 +32,7 @@ pub fn routes() -> Router<AppState> {
Router::new()
.route("/auth/challenge", post(create_challenge))
.route("/auth/verify", post(verify_challenge))
.route("/auth/validate", post(validate_token_endpoint))
}
fn now_ts() -> i64 {
@@ -172,8 +173,6 @@ async fn verify_challenge(
}
/// Validate a bearer token. Returns the fingerprint if valid.
/// Used by protected endpoints (will be wired in when auth middleware is added).
#[allow(dead_code)]
pub fn validate_token(db: &sled::Tree, token: &str) -> Option<String> {
let data = db.get(token.as_bytes()).ok()??;
let val: serde_json::Value = serde_json::from_slice(&data).ok()?;
@@ -184,3 +183,42 @@ pub fn validate_token(db: &sled::Tree, token: &str) -> Option<String> {
}
val.get("fingerprint")?.as_str().map(String::from)
}
#[derive(Deserialize)]
struct ValidateRequest {
token: String,
}
/// External token validation endpoint — used by WarzonePhone and other services
/// to verify that a bearer token is valid and get the associated fingerprint.
///
/// POST /v1/auth/validate { "token": "..." }
/// Returns: { "valid": true, "fingerprint": "...", "expires_at": ... }
/// or: { "valid": false }
async fn validate_token_endpoint(
State(state): State<AppState>,
Json(req): Json<ValidateRequest>,
) -> Json<serde_json::Value> {
match validate_token(&state.db.tokens, &req.token) {
Some(fingerprint) => {
// Also resolve alias if available
let alias = state.db.aliases.get(format!("fp:{}", fingerprint).as_bytes())
.ok().flatten()
.map(|v| String::from_utf8_lossy(&v).to_string());
// Get Ethereum address if we have the bundle
let eth_address: Option<String> = None; // Would need seed, which server doesn't have
tracing::info!("Token validated for {}", fingerprint);
Json(serde_json::json!({
"valid": true,
"fingerprint": fingerprint,
"alias": alias,
"eth_address": eth_address,
}))
}
None => {
Json(serde_json::json!({ "valid": false }))
}
}
}

View File

@@ -22,6 +22,7 @@ fn extract_message_id(data: &[u8]) -> Option<String> {
WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. } => {
Some(format!("skd:{}:{}", sender_fingerprint, group_name))
}
WireMessage::CallSignal { id, .. } => Some(id),
}
} else {
None

View File

@@ -34,6 +34,7 @@ fn extract_message_id(data: &[u8]) -> Option<String> {
WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. } => {
Some(format!("skd:{}:{}", sender_fingerprint, group_name))
}
WireMessage::CallSignal { id, .. } => Some(id),
}
} else {
None