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:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -2647,7 +2647,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-client"
|
name = "warzone-client"
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2680,7 +2680,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-mule"
|
name = "warzone-mule"
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2689,7 +2689,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
@@ -2712,7 +2712,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-server"
|
name = "warzone-server"
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2739,7 +2739,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-wasm"
|
name = "warzone-wasm"
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub fn routes() -> Router<AppState> {
|
|||||||
.route("/keys/list", get(list_keys))
|
.route("/keys/list", get(list_keys))
|
||||||
.route("/keys/:fingerprint", get(get_bundle))
|
.route("/keys/:fingerprint", get(get_bundle))
|
||||||
.route("/keys/:fingerprint/otpk-count", get(otpk_count))
|
.route("/keys/:fingerprint/otpk-count", get(otpk_count))
|
||||||
|
.route("/keys/:fingerprint/devices", get(list_devices))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Debug endpoint: list all registered fingerprints.
|
/// Debug endpoint: list all registered fingerprints.
|
||||||
@@ -42,6 +43,8 @@ fn normalize_fp(fp: &str) -> String {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct RegisterRequest {
|
struct RegisterRequest {
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
|
#[serde(default)]
|
||||||
|
device_id: Option<String>,
|
||||||
bundle: Vec<u8>,
|
bundle: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +57,17 @@ async fn register_keys(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<RegisterRequest>,
|
Json(req): Json<RegisterRequest>,
|
||||||
) -> Json<RegisterResponse> {
|
) -> Json<RegisterResponse> {
|
||||||
let key = normalize_fp(&req.fingerprint);
|
let fp = normalize_fp(&req.fingerprint);
|
||||||
tracing::info!("Registering bundle for {}", key);
|
let device_id = req.device_id.unwrap_or_else(|| "default".to_string());
|
||||||
let _ = state.db.keys.insert(key.as_bytes(), req.bundle);
|
|
||||||
|
// 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 })
|
Json(RegisterResponse { ok: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,3 +147,22 @@ async fn replenish_otpks(
|
|||||||
tracing::info!("Replenished {} OTPKs for {} (total: {})", stored, fp, total);
|
tracing::info!("Replenished {} OTPKs for {} (total: {})", stored, fp, total);
|
||||||
Json(serde_json::json!({ "ok": true, "stored": stored, "total": 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() }))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user