qualia_client_core/identity_plane/
device.rs1use crate::setup::DeviceContext;
4use serde::{Deserialize, Serialize};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7fn now_unix() -> u64 {
8 SystemTime::now()
9 .duration_since(UNIX_EPOCH)
10 .map(|d| d.as_secs())
11 .unwrap_or(0)
12}
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
16pub struct DeviceCapabilities {
17 #[serde(default = "default_true")]
19 pub local_jobs: bool,
20 #[serde(default = "default_true")]
22 pub inference: bool,
23 #[serde(default)]
25 pub mesh_transport: bool,
26}
27
28fn default_true() -> bool {
29 true
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct DeviceRecord {
35 pub device_id: String,
37 pub person_id: String,
39 pub identity_pubkey_hex: String,
41 #[serde(default)]
43 pub label: String,
44 #[serde(default)]
46 pub hostname: String,
47 #[serde(default)]
49 pub device_context: DeviceContext,
50 pub capabilities: DeviceCapabilities,
51 #[serde(default)]
54 pub control_base_url: String,
55 pub is_local: bool,
57 pub created_at_unix: u64,
58 pub last_seen_unix: u64,
59}
60
61pub type DeviceRecordPublic = DeviceRecord;
63
64impl DeviceRecord {
65 pub fn device_did_from_pubkey_hex(pubkey_hex: &str) -> String {
66 format!("did:q42:device:{}", pubkey_hex.trim().to_ascii_lowercase())
67 }
68
69 pub fn new_local(
70 person_id: impl Into<String>,
71 identity_pubkey_hex: impl Into<String>,
72 device_context: DeviceContext,
73 label: impl Into<String>,
74 ) -> Self {
75 let identity_pubkey_hex = identity_pubkey_hex.into().to_ascii_lowercase();
76 let device_id = Self::device_did_from_pubkey_hex(&identity_pubkey_hex);
77 let now = now_unix();
78 let hostname = hostname_best_effort();
79 Self {
80 device_id,
81 person_id: person_id.into(),
82 identity_pubkey_hex,
83 label: label.into(),
84 hostname,
85 device_context,
86 capabilities: DeviceCapabilities {
87 local_jobs: true,
88 inference: true,
89 mesh_transport: true,
90 },
91 control_base_url: String::new(),
92 is_local: true,
93 created_at_unix: now,
94 last_seen_unix: now,
95 }
96 }
97
98 pub fn touch(&mut self) {
99 self.last_seen_unix = now_unix();
100 }
101
102 pub fn with_control_base_url(mut self, url: impl Into<String>) -> Self {
103 self.control_base_url = url.into().trim().trim_end_matches('/').to_string();
104 self
105 }
106}
107
108fn hostname_best_effort() -> String {
109 std::env::var("COMPUTERNAME")
110 .or_else(|_| std::env::var("HOSTNAME"))
111 .unwrap_or_default()
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn device_id_is_not_person_id_shape() {
120 let d = DeviceRecord::new_local(
121 "did:q42:person:aabb",
122 "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
123 DeviceContext::default(),
124 "desk",
125 );
126 assert!(d.device_id.starts_with("did:q42:device:"));
127 assert!(!d.device_id.starts_with("did:q42:person:"));
128 assert_ne!(d.device_id, d.person_id);
129 }
130}