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]]
|
||||
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",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.20"
|
||||
version = "0.0.21"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
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, .. }) => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user