feat(mobile): per-user device bindings, multi-user Inbox routing, and admin-mediated authorization

- Device→user bindings persisted in config table (auth.rs), loaded at plugin start
- RelayApp now routes Inbox responses per-user via UserChannelApi, never globally
- New mobile_bind_device LLM tool for admin-mediated device→user assignment
- Per-user event forwarders (events.rs) with per-user debounced notifiers
- Config listener (auth::config_listener) refreshes bindings cache reactively
- Reconcile loop catches users who unlock after boot
- Hello/Logout treated as device-registry ops (no user resolution needed)
- Unbound device payloads are silently dropped
- RelayAgent::authorize_client → bind_device (atomic bind + authorize)
- Approval rules seed mobile_bind_device/revoke_device as require
This commit is contained in:
2026-07-11 11:18:35 +01:00
parent a847dda88f
commit 2c54778116
15 changed files with 856 additions and 221 deletions
+12 -10
View File
@@ -31,6 +31,8 @@ pub struct ClientInfo {
pub platform: Option<String>,
/// Unix ms of last activity, if any.
pub last_seen: Option<i64>,
/// The Skald user this device is bound to, if any (blueprint §13).
pub bound_user: Option<String>,
}
/// The control API exposed by the plugin. Reachable via
@@ -50,18 +52,18 @@ pub trait RelayAgent: Send + Sync {
/// Derived namespace id (hex).
fn namespace_id(&self) -> String;
/// Send the current Inbox snapshot to all authorized clients.
async fn broadcast_inbox(&self) -> anyhow::Result<()>;
/// Generic push notification to all authorized clients.
async fn broadcast_notification(&self, title: &str, body: &str) -> anyhow::Result<()>;
/// List all known devices.
/// List all known devices, each tagged with its bound user (if any).
async fn list_clients(&self) -> Vec<ClientInfo>;
/// Authorize a Pending device by its ed25519 pubkey.
async fn authorize_client(&self, ed25519_pub: [u8; 32]) -> anyhow::Result<()>;
/// Bind a paired device to a Skald user and authorize it (blueprint §13,
/// admin-mediated — the mobile analogue of `telegram_pairing`).
async fn bind_device(
&self,
ed25519_pub: [u8; 32],
user_id: String,
display: Option<String>,
) -> anyhow::Result<()>;
/// Revoke a device (lost/stolen) by its ed25519 pubkey.
/// Revoke a device (lost/stolen) by its ed25519 pubkey and drop its binding.
async fn revoke_client(&self, ed25519_pub: [u8; 32]) -> anyhow::Result<()>;
}