Skip to main content

ai_cortex_sdk/
client.rs

1use std::time::Duration;
2
3use crate::config::CortexConfig;
4use crate::error::{SdkError, SdkResult};
5use crate::models::*;
6
7/// AI Cortex SDK 客户端。
8///
9/// 鉴权方式:用户 PAT(`Authorization: Bearer actx_pat_...`),由配置直接提供。
10/// 构造后自动启动心跳(除非 `heartbeat_interval == 0`),首次心跳即向服务端绑定当前设备。
11pub struct CortexClient {
12    config: CortexConfig,
13    http: reqwest::Client,
14    heartbeat_handle: Option<tokio::task::JoinHandle<()>>,
15}
16
17impl CortexClient {
18    pub fn new(config: CortexConfig) -> Self {
19        let http = reqwest::Client::builder()
20            .timeout(Duration::from_secs(config.timeout))
21            .build()
22            .expect("Failed to create HTTP client");
23        let mut client = Self {
24            config,
25            http,
26            heartbeat_handle: None,
27        };
28        client.start_heartbeat();
29        client
30    }
31
32    fn bearer(&self) -> String {
33        format!("Bearer {}", self.config.pat)
34    }
35
36    fn auth_get(&self, url: &str) -> reqwest::RequestBuilder {
37        self.http.get(url).header("Authorization", self.bearer())
38    }
39
40    fn auth_post(&self, url: &str, body: &impl serde::Serialize) -> reqwest::RequestBuilder {
41        self.http
42            .post(url)
43            .header("Authorization", self.bearer())
44            .json(body)
45    }
46
47    async fn decode<T: serde::de::DeserializeOwned>(&self, resp: reqwest::Response) -> SdkResult<T> {
48        let api_resp: ApiResponse<T> = resp.json().await?;
49        if !api_resp.success {
50            return Err(SdkError::ServerError(api_resp.message.unwrap_or_default()));
51        }
52        api_resp
53            .data
54            .ok_or_else(|| SdkError::ServerError("No data".to_string()))
55    }
56
57    // --- Software Store APIs ---
58
59    pub async fn list_software(&self) -> SdkResult<Vec<SoftwareStoreItem>> {
60        let url = format!("{}/api/softwares/store", self.config.server_url);
61        let resp = self.auth_get(&url).send().await?;
62        self.decode(resp).await
63    }
64
65    pub async fn get_latest_version(&self, software_id: &str) -> SdkResult<SoftwareVersion> {
66        let url = format!(
67            "{}/api/softwares/{}/latest-version",
68            self.config.server_url, software_id
69        );
70        let resp = self.auth_get(&url).send().await?;
71        self.decode(resp).await
72    }
73
74    pub async fn download(&self, version_id: &str) -> SdkResult<DownloadInfo> {
75        let url = format!(
76            "{}/api/softwares/sdk-download/{}",
77            self.config.server_url, version_id
78        );
79        let resp = self.auth_post(&url, &serde_json::json!({})).send().await?;
80        self.decode(resp).await
81    }
82
83    // --- Auto Update APIs ---
84
85    /// 检查更新(Pull)。按配置的 software_id + 指定 platform/channel,与 current_version 比较。
86    /// `channel` 传 None 默认 "stable"。需对该 software 持有有效许可证,否则返回 ServerError。
87    pub async fn check_update(
88        &self,
89        platform: &str,
90        current_version: &str,
91        channel: Option<&str>,
92    ) -> SdkResult<UpdateInfo> {
93        let url = format!("{}/api/softwares/sdk-check-update", self.config.server_url);
94        let body = serde_json::json!({
95            "software_id": self.config.software_id,
96            "platform": platform,
97            "channel": channel.unwrap_or("stable"),
98            "current_version": current_version,
99        });
100        let resp = self.auth_post(&url, &body).send().await?;
101        self.decode(resp).await
102    }
103
104    /// 打开更新事件 SSE 流(Push)。`software_id` 传 None 订阅全部;`last_event_id` 用于断线重连补播。
105    /// 返回的流需配合 `futures_util::StreamExt` 使用:`while let Some(ev) = stream.next().await { ... }`。
106    /// 注意:SSE 是长连接,内部使用无读超时的 HTTP 客户端;靠服务端 15s keepalive 保活。
107    pub async fn open_update_events(
108        &self,
109        software_id: Option<&str>,
110        last_event_id: Option<u64>,
111    ) -> SdkResult<crate::sse::UpdateEventStream> {
112        // 用一个无读超时的专用 client,避免长连接被 self.http 的 30s timeout 切断
113        let stream_client = reqwest::Client::builder().build()?;
114        let url = format!("{}/api/softwares/sdk-update-events", self.config.server_url);
115        let mut req = stream_client
116            .get(&url)
117            .header("Authorization", self.bearer())
118            .header("Accept", "text/event-stream");
119        if let Some(sid) = software_id {
120            req = req.query(&[("software_id", sid)]);
121        }
122        if let Some(last) = last_event_id {
123            req = req.header("Last-Event-ID", last.to_string());
124        }
125        let resp = req.send().await?;
126        if !resp.status().is_success() {
127            return Err(SdkError::ServerError(format!(
128                "SSE connect failed: HTTP {}",
129                resp.status()
130            )));
131        }
132        // 将 bytes chunk 转为 Vec<u8>,避免引入 bytes crate 依赖
133        let stream = futures_util::StreamExt::map(resp.bytes_stream(), |r| {
134            r.map(|b| b.to_vec())
135        });
136        Ok(crate::sse::UpdateEventStream::new(stream))
137    }
138
139    // --- Device APIs(user 维度,user 由 PAT 推断)---
140
141    /// 列出当前用户名下绑定的有效设备。
142    pub async fn list_devices(&self) -> SdkResult<Vec<DeviceRecord>> {
143        let url = format!("{}/api/devices/active", self.config.server_url);
144        let resp = self.auth_get(&url).send().await?;
145        self.decode(resp).await
146    }
147
148    /// 解绑指定设备(将其 status 置 0,释放设备名额)。
149    pub async fn unbind_device(&self, device_id: &str) -> SdkResult<()> {
150        let url = format!("{}/api/devices/{}/unbind", self.config.server_url, device_id);
151        let resp = self.auth_post(&url, &serde_json::json!({})).send().await?;
152        let api_resp: ApiResponse<serde_json::Value> = resp.json().await?;
153        if !api_resp.success {
154            return Err(SdkError::ServerError(api_resp.message.unwrap_or_default()));
155        }
156        Ok(())
157    }
158
159    // --- Offline license ---
160
161    /// 向服务端拉取离线许可证(需有效 PAT 与有效软件授权)。
162    /// `fingerprint` 为当前设备指纹。返回未验签的许可证文件,需配合
163    /// [`verify_offline_license`] 进行本地验签后才可信任。
164    pub async fn fetch_offline_license(&self, fingerprint: &str) -> SdkResult<OfflineLicenseFile> {
165        let url = format!("{}/api/offline-licenses/issue", self.config.server_url);
166        let body = serde_json::json!({
167            "softwareId": self.config.software_id,
168            "fingerprint": fingerprint,
169        });
170        let resp = self.auth_post(&url, &body).send().await?;
171        self.decode(resp).await
172    }
173
174    /// 使用配置中的 `software_public_key`(若设置)验签当前许可证并解析 payload。
175    /// 若未钉扎公钥,则信任服务端下发公钥(依赖 TLS)。
176    pub fn verify_offline_license(
177        &self,
178        file: &OfflineLicenseFile,
179        expected_fingerprint: &str,
180    ) -> SdkResult<OfflineLicensePayload> {
181        verify_offline_license(
182            file,
183            &self.config.software_id,
184            expected_fingerprint,
185            self.config.software_public_key.as_deref(),
186        )
187    }
188
189    /// 启动后台心跳任务:每隔 heartbeat_interval 秒上报设备指纹。
190    /// 首次心跳即向服务端绑定设备(按 config.software_id 对应许可证的 max_devices 校验上限);
191    /// 后续刷新 last_active_time。间隔为 0 时不启动;重复调用会先停掉旧任务。
192    fn start_heartbeat(&mut self) {
193        if let Some(h) = self.heartbeat_handle.take() {
194            h.abort();
195        }
196        if self.config.heartbeat_interval == 0 {
197            return;
198        }
199        let http = self.http.clone();
200        let config = self.config.clone();
201        let interval = self.config.heartbeat_interval;
202        let handle = tokio::spawn(async move {
203            let mut ticker = tokio::time::interval(Duration::from_secs(interval));
204            loop {
205                ticker.tick().await;
206                let dev = crate::device::collect();
207                let url = format!("{}/api/devices/heartbeat", config.server_url);
208                // 心跳失败静默处理,不阻塞客户端主流程
209                let _ = http
210                    .post(&url)
211                    .header("Authorization", format!("Bearer {}", config.pat))
212                    .json(&serde_json::json!({
213                        "softwareId": config.software_id,
214                        "fingerprint": dev.fingerprint,
215                        "deviceInfo": dev.info,
216                    }))
217                    .send()
218                    .await;
219            }
220        });
221        self.heartbeat_handle = Some(handle);
222    }
223}
224
225impl Drop for CortexClient {
226    fn drop(&mut self) {
227        if let Some(h) = self.heartbeat_handle.take() {
228            h.abort();
229        }
230    }
231}
232
233/// 验证离线许可证并解析 payload,校验顺序:
234///
235/// 1. **公钥钉扎**:若 `pinned_public_key` 为 `Some`,要求 `file.public_key` 与之严格相等,
236///    否则直接拒绝(防中间人替换公钥)。
237/// 2. **Ed25519 验签**:用 `public_key`(hex → 32 字节)对 `payload` 的 UTF-8 字节验签。
238/// 3. **解析 payload**:将 `payload` JSON 反序列化为 [`OfflineLicensePayload`]。
239/// 4. **字段匹配**:`payload.software_id == expected_software_id` 且
240///    `payload.fingerprint == expected_fingerprint`。
241/// 5. **过期校验**:`payload.expire_time > now`(now 取 `SystemTime::now` 的 Unix 秒)。
242///
243/// 全部通过返回 [`OfflineLicensePayload`],任一失败返回对应的 [`SdkError`]。
244pub fn verify_offline_license(
245    file: &OfflineLicenseFile,
246    expected_software_id: &str,
247    expected_fingerprint: &str,
248    pinned_public_key: Option<&str>,
249) -> SdkResult<OfflineLicensePayload> {
250    // 1. 公钥钉扎
251    if let Some(pinned) = pinned_public_key {
252        if pinned.trim() != file.public_key.trim() {
253            return Err(SdkError::LicenseFieldMismatch(format!(
254                "public_key mismatch: pinned {:?} got {:?}",
255                pinned, file.public_key
256            )));
257        }
258    }
259
260    // 2. Ed25519 验签
261    let pub_bytes = hex::decode(&file.public_key).map_err(|e| {
262        SdkError::Signature(format!("invalid public_key hex: {e}"))
263    })?;
264    if pub_bytes.len() != 32 {
265        return Err(SdkError::Signature(format!(
266            "public_key must be 32 bytes, got {}",
267            pub_bytes.len()
268        )));
269    }
270    let mut pk_arr = [0u8; 32];
271    pk_arr.copy_from_slice(&pub_bytes);
272    let vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
273        .map_err(|e| SdkError::Signature(format!("invalid verifying key: {e}")))?;
274
275    let sig_bytes = hex::decode(&file.signature).map_err(|e| {
276        SdkError::Signature(format!("invalid signature hex: {e}"))
277    })?;
278    if sig_bytes.len() != 64 {
279        return Err(SdkError::Signature(format!(
280            "signature must be 64 bytes, got {}",
281            sig_bytes.len()
282        )));
283    }
284    let sig = ed25519_dalek::Signature::from_slice(&sig_bytes)
285        .map_err(|e| SdkError::Signature(format!("invalid signature: {e}")))?;
286
287    use ed25519_dalek::Verifier;
288    vk
289        .verify(file.payload.as_bytes(), &sig)
290        .map_err(|_| SdkError::Signature("signature verification failed".to_string()))?;
291
292    // 3. 解析 payload
293    let payload: OfflineLicensePayload = serde_json::from_str(&file.payload).map_err(|e| {
294        SdkError::Signature(format!("invalid payload json: {e}"))
295    })?;
296
297    // 4. 字段匹配
298    if payload.software_id != expected_software_id {
299        return Err(SdkError::LicenseFieldMismatch(format!(
300            "software_id mismatch: expected {expected_software_id:?} got {:?}",
301            payload.software_id
302        )));
303    }
304    if payload.fingerprint != expected_fingerprint {
305        return Err(SdkError::LicenseFieldMismatch(format!(
306            "fingerprint mismatch: expected {expected_fingerprint:?} got {:?}",
307            payload.fingerprint
308        )));
309    }
310
311    // 5. 过期校验
312    let now = std::time::SystemTime::now()
313        .duration_since(std::time::UNIX_EPOCH)
314        .map(|d| d.as_secs() as i64)
315        .unwrap_or(0);
316    if payload.expire_time <= now {
317        return Err(SdkError::LicenseExpired);
318    }
319
320    Ok(payload)
321}