ai-cortex-sdk 0.1.0

Rust client SDK for AI Cortex server: PAT auth, device binding, software store, offline license (Ed25519), and auto-update (check-update pull + SSE push).
Documentation

ai-cortex-sdk

Rust SDK for AI Cortex server — authentication, authorization, and software store APIs.

Installation

Add to your Cargo.toml:

[dependencies]
ai-cortex-sdk = { path = "../crates/ai-cortex-sdk" }

Quick Start

use ai_cortex_sdk::{CortexClient, CortexConfig};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config = CortexConfig::new("http://localhost:40404", "your-app-key");
    let mut client = CortexClient::new(config);

    // Login
    let user = client.login("admin", "password").await?;
    println!("Logged in as: {}", user.display_name);

    // Check authorization for an app
    let authorized = client.check_auth("target-app-key").await?;
    println!("Authorized: {}", authorized);

    // Browse software store
    let items = client.list_software().await?;
    for item in &items {
        println!("{}: {:?}", item.software.name, item.latest_version.as_ref().map(|v| &v.version));
    }

    // Download latest version
    if let Some(item) = items.first() {
        if let Some(version) = &item.latest_version {
            let download = client.download(&version.id).await?;
            println!("Download URL: {}", download.download_url);
        }
    }

    // Logout
    client.logout().await?;
    Ok(())
}

Configuration

// server_url, PAT, software_id(软件身份,心跳/下载按该软件许可证校验)
let config = CortexConfig::new("http://localhost:40404", "actx_pat_...", "software-uuid");

// Custom timeout
let config = CortexConfig::new("http://localhost:40404", "actx_pat_...", "software-uuid")
    .with_timeout(60);

API Reference

Authentication

Method Description
client.login(username, password) Login and store session token
client.logout() Logout and clear session
client.is_logged_in() Check if currently authenticated

Authorization

Method Description
client.get_auth_info() Get current user's auth records and associated apps
client.check_auth(app_key) Check if current user is authorized for a specific app

Software Store

Method Description
client.list_software() List all software with latest versions
client.get_latest_version(software_id) Get latest version of a software
client.download(version_id) Get download URL for a version (requires auth)

Auto Update (自动更新)

提供 Pull(轮询)Push(SSE 订阅) 两种更新检测方式。两者都需对该 software 持有有效许可证。

Method Description
client.check_update(platform, current_version, channel) 主动检查更新(Pull),返回 UpdateInfo
client.open_update_events(software_id, last_event_id) 打开更新事件 SSE 流(Push),返回 UpdateEventStream

Pull — check_update

// 按 platform + channel 与 current_version 比较;channel 传 None 默认 "stable"
let info = client.check_update("linux", "1.0.0", None).await?;
// UpdateInfo { has_update, force_update, current_version, target_version: Option<VersionBrief> }
println!("has_update={}, force_update={}", info.has_update, info.force_update);
if let Some(t) = &info.target_version {
    println!("可升级到 {} (channel={}, {} bytes)", t.version, t.channel, t.file_size);
}
// force_update 阻塞语义:为 true 时客户端应阻止启动,引导用户升级后再放行

Push — open_update_events(SSE 长连接)

服务端通过 GET /api/softwares/sdk-update-eventstext/event-stream,PAT 鉴权)主动推送新版本事件, 无需客户端轮询。返回的流需配合 futures_util::StreamExt 使用:

use futures_util::StreamExt;

// software_id 传 None 订阅全部;last_event_id 用于断线重连补播
let mut stream = client.open_update_events(Some("software-uuid"), None).await?;
while let Some(ev) = stream.next().await {
    match ev {
        Ok(e) => {
            // e.id 为单调递增序号;e.force_update 标记是否强制
            println!("update #{}: v{} force={}", e.id, e.version, e.force_update);
        }
        Err(e) => eprintln!("stream error: {e}"),
    }
}

说明:

  • SSE 是长连接,内部使用无读超时的 HTTP 客户端(绕过 CortexConfig.timeout 的 30s 默认值), 靠服务端 15s keepalive 注释帧保活。
  • 断线重连时把最后一次收到的 event.id 作为 last_event_id 传入,服务端补播 id > last 的事件。
  • 服务端会下发 : keepalive / : lagged 注释帧,SDK 内部自动忽略。

Error Handling

use ai_cortex_sdk::SdkError;

match client.login("user", "pass").await {
    Ok(info) => println!("Welcome {}", info.display_name),
    Err(SdkError::NotAuthenticated) => eprintln!("Not logged in"),
    Err(SdkError::Unauthorized(reason)) => eprintln!("Unauthorized: {}", reason),
    Err(SdkError::RequestError(e)) => eprintln!("Network error: {}", e),
    Err(SdkError::ServerError(msg)) => eprintln!("Server error: {}", msg),
}

Examples

See examples/ directory for complete usage examples.

Run an example:

cargo run --example basic
cargo run --example offline_license -- <PAT> <SOFTWARE_ID> [PINNED_PUBKEY_HEX]
cargo run --example auto_update -- <PAT> <SOFTWARE_ID> [stream]

离线许可证

服务端可在 PAT 鉴权下通过 POST /api/offline-licenses/issue 下发一份签名许可证文件, 让客户端在受限网络/断网环境下仍能本地证明自己持有有效授权。文件结构:

{
  "payload":   "<LicensePayload 的 JSON 字符串>",
  "signature": "<hex 64 字节 Ed25519 签名>",
  "public_key":"<hex 32 字节 Ed25519 公钥>"
}

payloadOfflineLicensePayload 的 snake_case JSON,包含 license_id / user_id / software_id / fingerprint / max_devices / expire_time / issued_at。 签名是对 payload 字符串 UTF-8 字节的 Ed25519 签名。

流程

use ai_cortex_sdk::{verify_offline_license, CortexClient, CortexConfig};

// 1. 配置(可选钉扎公钥,增强防替换)
let config = CortexConfig::new("http://localhost:40404", "actx_pat_...", "software-uuid")
    .with_software_public_key("abcdef...32字节hex公钥");

let client = CortexClient::new(config);
let fp = ai_cortex_sdk::device::collect().fingerprint;

// 2. 在线拉取(需联网 + 有效 PAT + 有效软件授权)
let file = client.fetch_offline_license(&fp).await?;

// 3. 本地验签:Ed25519 验签 → 解析 payload → 校验 software_id/fingerprint → 校验未过期
let payload = verify_offline_license(&file, "software-uuid", &fp, Some("abcdef..."))?;
// 或用 client.verify_offline_license(&file, &fp) 自动带入配置的 software_id 与钉扎公钥

撤销语义与短 TTL

许可证的 expire_time = issued_at + 86400(1 天 TTL)。撤销依赖在线刷新: 许可证本身是自包含签名文件,服务端无法主动吊销已下发的离线文件;通过短 TTL 强制客户端 周期性联网 fetch_offline_license 重新换取。若服务端撤销了用户的软件授权或解绑了该设备指纹, 下一次 fetch 将失败,客户端即失去有效许可证,从而实现“软撤销”。

可选公钥钉扎

默认情况下客户端信任 issue 响应中下发的 public_key(依赖 TLS 保证传输不被篡改)。 对安全敏感场景,可通过 CortexConfig::with_software_public_key(hex) 钉扎一个预期公钥: 设置后 verify_offline_license 会要求下发公钥与之严格相等,否则直接判定为 LicenseFieldMismatch,可防御 TLS 被绕过/中间人替换公钥的攻击。不设置时则退化为信任下发公钥。

校验顺序

verify_offline_license 按以下顺序校验,任一失败立即返回对应 SdkError

  1. 公钥钉扎(若提供)—— public_key 必须与钉扎值相等
  2. Ed25519 验签 —— public_keypayload 字节验签
  3. 解析 payload JSON
  4. 字段匹配 —— software_idfingerprint 必须与期望值一致
  5. 过期校验 —— expire_time > now