ai-cortex-sdk 0.1.1

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

crates.io docs.rs license

Rust client SDK for the AI Cortex server — PAT 鉴权、设备绑定、软件商店、自动更新(check-update + SSE)、离线许可证(Ed25519 签名)。

Installation

[dependencies]
ai-cortex-sdk = "0.1"

Quick Start

SDK 用用户 PATactx_pat_...)鉴权,PAT 由用户在服务端 web 控制台调 POST /api/access-tokens 创建。

use ai_cortex_sdk::{CortexClient, CortexConfig};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // server_url, PAT, software_id(SDK 以该软件身份心跳 / 下载 / 检查更新)
    let config = CortexConfig::new("http://localhost:40404", "actx_pat_...", "software-uuid");
    let client = CortexClient::new(config);

    // 软件商店:列出软件及其最新版本
    for item in client.list_software().await? {
        let latest = item.latest_version.as_ref().map(|v| v.version.as_str()).unwrap_or("-");
        println!("{} -> {}", item.software.name, latest);
    }

    // 检查自动更新(Pull)
    let info = client.check_update("linux", "1.0.0", None).await?;
    if info.has_update {
        println!("有新版本,强制升级: {}", info.force_update);
    }

    Ok(())
}

Configuration

let config = CortexConfig::new("http://localhost:40404", "actx_pat_...", "software-uuid")
    .with_timeout(60)              // 请求超时(秒),默认 30
    .with_heartbeat_interval(60)   // 心跳间隔(秒);0 = 关闭,默认 60
    .without_heartbeat()           // 或直接关闭心跳
    .with_software_public_key("abcdef...32字节hex公钥"); // 钉扎离线许可证公钥

CortexClient::new 会自动启动后台心跳(首次即向服务端绑定当前设备,按该软件许可证的 max_devices 校验上限);Drop 时自动停止心跳。

API Reference

Software Store

Method Description
client.list_software() 列出所有软件及其最新活动版本
client.get_latest_version(software_id) 取某软件最新版本
client.download(version_id) 取某版本的下载地址(需许可证)

Auto Update(自动更新)

提供 Pull(主动检查)Push(SSE 订阅) 两种检测方式,互补使用:SSE 做实时通知,Pull 做准确判定 + 断线兜底。两者都需对该 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

// 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> }
if info.force_update {
    // 强制升级:客户端应阻止旧版启动,引导用户升级后再放行
} else if info.has_update {
    let t = info.target_version.unwrap();
    println!("可升级到 {} (channel={}, {} bytes)", t.version, t.channel, t.file_size);
}

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(Ok(ev)) = stream.next().await {
    // ev.id 单调递增;ev.force_update 标记是否强制
    println!("update #{}: v{} force={}", ev.id, ev.version, ev.force_update);
}

说明:

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

Device(设备)

Method Description
device::collect() 采集当前设备指纹(DeviceIdentity { fingerprint, info }
client.list_devices() 列出当前用户名下绑定的有效设备
client.unbind_device(device_id) 解绑设备(释放名额)

Error Handling

use ai_cortex_sdk::SdkError;

match client.check_update("linux", "1.0.0", None).await {
    Ok(info) => { /* ... */ }
    Err(SdkError::NotAuthenticated) => eprintln!("未认证 / PAT 失效"),
    Err(SdkError::Unauthorized(reason)) => eprintln!("无授权: {reason}"),
    Err(SdkError::RequestError(e)) => eprintln!("网络错误: {e}"),
    Err(SdkError::ServerError(msg)) => eprintln!("服务端错误: {msg}"),
    Err(e) => eprintln!("其它: {e}"),
}

Examples

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

License

MIT(见 LICENSE)。