feat(core): add event revisions and saved-device local labels

Platform contract harnesses need monotonic event revisions for at-least-once
dedup and a typed rename API that survives listing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 04:47:28 +02:00
parent 89c2206f0f
commit 8e8cab9b24
13 changed files with 162 additions and 19 deletions

View File

@@ -162,6 +162,11 @@ impl DeviceRelationshipService {
.execute(pool)
.await?;
}
if !has("local_label") {
sqlx::query("ALTER TABLE device_relationships ADD COLUMN local_label TEXT")
.execute(pool)
.await?;
}
Self::ensure_lifecycle_schema(pool).await?;
Ok(())
}
@@ -252,21 +257,58 @@ impl DeviceRelationshipService {
}
pub(crate) async fn list_saved_devices(&self) -> Result<Vec<SavedDevice>, VnidropError> {
Ok(self
.list()
.await?
let rows = sqlx::query(
r#"
SELECT remote_endpoint_id, local_label, created_at, updated_at
FROM device_relationships
WHERE state = 'saved'
ORDER BY updated_at DESC
"#,
)
.fetch_all(&self.pool)
.await
.map_err(VnidropError::repository)?;
Ok(rows
.into_iter()
.filter(|entry| entry.state == DeviceRelationshipState::Saved)
.map(|entry| SavedDevice {
endpoint_id: entry.remote_endpoint_id,
local_label: None,
.map(|row| SavedDevice {
endpoint_id: row.get("remote_endpoint_id"),
local_label: row.get("local_label"),
remote_display_name: None,
created_at: entry.created_at,
last_authenticated_at: Some(entry.updated_at),
created_at: row.get("created_at"),
last_authenticated_at: Some(row.get("updated_at")),
})
.collect())
}
/// Sets the user-owned local label for a Saved device. Labels are never
/// overwritten by remote display names.
pub(crate) async fn set_saved_device_label(
&self,
peer_endpoint_id: String,
label: Option<String>,
) -> Result<(), VnidropError> {
let result = sqlx::query(
r#"
UPDATE device_relationships
SET local_label = ?2, updated_at = ?3
WHERE remote_endpoint_id = ?1 AND state = 'saved'
"#,
)
.bind(&peer_endpoint_id)
.bind(label)
.bind(now_ms())
.execute(&self.pool)
.await
.map_err(VnidropError::repository)?;
if result.rows_affected() == 0 {
return Err(VnidropError::invalid_input(anyhow::anyhow!(
"peer is not a saved device"
)));
}
self.emit_changed(&peer_endpoint_id, DeviceRelationshipState::Saved);
Ok(())
}
pub(crate) async fn request_pairing(
self: &Arc<Self>,
peer_endpoint_id: String,