v0.0.11: Multi-device support (server-side)

Server:
- Register stores per-device bundles: device:<fp>:<device_id>
- GET /v1/keys/:fp/devices lists all registered devices
- WS already pushes to ALL connected devices per fingerprint
- DB queue: first device to poll gets messages (acceptable for Phase 2)

Multi-device flow:
- Same seed on two devices → same fingerprint
- Both register with different device_ids
- Both connect via WS → both receive messages in real-time
- Each device maintains its own ratchet sessions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 12:52:22 +04:00
parent 9811248b7c
commit fff443bb6d
3 changed files with 39 additions and 9 deletions

10
warzone/Cargo.lock generated
View File

@@ -2647,7 +2647,7 @@ dependencies = [
[[package]]
name = "warzone-client"
version = "0.0.10"
version = "0.0.11"
dependencies = [
"anyhow",
"argon2",
@@ -2680,7 +2680,7 @@ dependencies = [
[[package]]
name = "warzone-mule"
version = "0.0.10"
version = "0.0.11"
dependencies = [
"anyhow",
"clap",
@@ -2689,7 +2689,7 @@ dependencies = [
[[package]]
name = "warzone-protocol"
version = "0.0.10"
version = "0.0.11"
dependencies = [
"base64",
"bincode",
@@ -2712,7 +2712,7 @@ dependencies = [
[[package]]
name = "warzone-server"
version = "0.0.10"
version = "0.0.11"
dependencies = [
"anyhow",
"axum",
@@ -2739,7 +2739,7 @@ dependencies = [
[[package]]
name = "warzone-wasm"
version = "0.0.10"
version = "0.0.11"
dependencies = [
"base64",
"bincode",

View File

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

View File

@@ -14,6 +14,7 @@ pub fn routes() -> Router<AppState> {
.route("/keys/list", get(list_keys))
.route("/keys/:fingerprint", get(get_bundle))
.route("/keys/:fingerprint/otpk-count", get(otpk_count))
.route("/keys/:fingerprint/devices", get(list_devices))
}
/// Debug endpoint: list all registered fingerprints.
@@ -42,6 +43,8 @@ fn normalize_fp(fp: &str) -> String {
#[derive(Deserialize)]
struct RegisterRequest {
fingerprint: String,
#[serde(default)]
device_id: Option<String>,
bundle: Vec<u8>,
}
@@ -54,9 +57,17 @@ async fn register_keys(
State(state): State<AppState>,
Json(req): Json<RegisterRequest>,
) -> Json<RegisterResponse> {
let key = normalize_fp(&req.fingerprint);
tracing::info!("Registering bundle for {}", key);
let _ = state.db.keys.insert(key.as_bytes(), req.bundle);
let fp = normalize_fp(&req.fingerprint);
let device_id = req.device_id.unwrap_or_else(|| "default".to_string());
// Store bundle keyed by fingerprint (primary, used for lookup)
let _ = state.db.keys.insert(fp.as_bytes(), req.bundle.clone());
// Also store per-device: device:<fp>:<device_id> → bundle
let device_key = format!("device:{}:{}", fp, device_id);
let _ = state.db.keys.insert(device_key.as_bytes(), req.bundle);
tracing::info!("Registered bundle for {} (device: {})", fp, device_id);
Json(RegisterResponse { ok: true })
}
@@ -136,3 +147,22 @@ async fn replenish_otpks(
tracing::info!("Replenished {} OTPKs for {} (total: {})", stored, fp, total);
Json(serde_json::json!({ "ok": true, "stored": stored, "total": total }))
}
/// List all registered devices for a fingerprint.
async fn list_devices(
State(state): State<AppState>,
Path(fingerprint): Path<String>,
) -> Json<serde_json::Value> {
let fp = normalize_fp(&fingerprint);
let prefix = format!("device:{}:", fp);
let devices: Vec<String> = state.db.keys.scan_prefix(prefix.as_bytes())
.filter_map(|item| {
item.ok().and_then(|(k, _)| {
let key_str = String::from_utf8_lossy(&k).to_string();
// key format: device:<fp>:<device_id>
key_str.rsplit(':').next().map(|s| s.to_string())
})
})
.collect();
Json(serde_json::json!({ "fingerprint": fp, "devices": devices, "count": devices.len() }))
}