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:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -2789,7 +2789,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-client"
|
name = "warzone-client"
|
||||||
version = "0.0.19"
|
version = "0.0.20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2822,7 +2822,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-mule"
|
name = "warzone-mule"
|
||||||
version = "0.0.19"
|
version = "0.0.20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2831,7 +2831,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.19"
|
version = "0.0.20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
@@ -2856,7 +2856,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-server"
|
name = "warzone-server"
|
||||||
version = "0.0.19"
|
version = "0.0.20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2883,7 +2883,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-wasm"
|
name = "warzone-wasm"
|
||||||
version = "0.0.19"
|
version = "0.0.20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.20"
|
version = "0.0.21"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ pub async fn run(server_url: &str, identity: &IdentityKeyPair) -> Result<()> {
|
|||||||
Ok(WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. }) => {
|
Ok(WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. }) => {
|
||||||
println!(" [sender key] received key from {} for #{}", 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) => {
|
Err(e) => {
|
||||||
eprintln!(" failed to deserialize message: {}", e);
|
eprintln!(" failed to deserialize message: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1542,6 +1542,22 @@ fn process_wire_message(
|
|||||||
message_id: None,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,4 +101,34 @@ pub enum WireMessage {
|
|||||||
chain_key: [u8; 32],
|
chain_key: [u8; 32],
|
||||||
generation: u32,
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub fn routes() -> Router<AppState> {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/auth/challenge", post(create_challenge))
|
.route("/auth/challenge", post(create_challenge))
|
||||||
.route("/auth/verify", post(verify_challenge))
|
.route("/auth/verify", post(verify_challenge))
|
||||||
|
.route("/auth/validate", post(validate_token_endpoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn now_ts() -> i64 {
|
fn now_ts() -> i64 {
|
||||||
@@ -172,8 +173,6 @@ async fn verify_challenge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Validate a bearer token. Returns the fingerprint if valid.
|
/// 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> {
|
pub fn validate_token(db: &sled::Tree, token: &str) -> Option<String> {
|
||||||
let data = db.get(token.as_bytes()).ok()??;
|
let data = db.get(token.as_bytes()).ok()??;
|
||||||
let val: serde_json::Value = serde_json::from_slice(&data).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)
|
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 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ fn extract_message_id(data: &[u8]) -> Option<String> {
|
|||||||
WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. } => {
|
WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. } => {
|
||||||
Some(format!("skd:{}:{}", sender_fingerprint, group_name))
|
Some(format!("skd:{}:{}", sender_fingerprint, group_name))
|
||||||
}
|
}
|
||||||
|
WireMessage::CallSignal { id, .. } => Some(id),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ fn extract_message_id(data: &[u8]) -> Option<String> {
|
|||||||
WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. } => {
|
WireMessage::SenderKeyDistribution { sender_fingerprint, group_name, .. } => {
|
||||||
Some(format!("skd:{}:{}", sender_fingerprint, group_name))
|
Some(format!("skd:{}:{}", sender_fingerprint, group_name))
|
||||||
}
|
}
|
||||||
|
WireMessage::CallSignal { id, .. } => Some(id),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
Reference in New Issue
Block a user