From 9bb5cbba64923f293222b483400f939c8a92a3e4 Mon Sep 17 00:00:00 2001 From: huangping Date: Mon, 18 May 2026 21:21:15 +0800 Subject: [PATCH] docs: complete implementation plan for selfhosted licensing SDK (25 tasks, 4 phases) --- .../2026-05-18-selfhosted-licensing-sdk.md | 575 ++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-18-selfhosted-licensing-sdk.md diff --git a/docs/superpowers/plans/2026-05-18-selfhosted-licensing-sdk.md b/docs/superpowers/plans/2026-05-18-selfhosted-licensing-sdk.md new file mode 100644 index 0000000..1eef63a --- /dev/null +++ b/docs/superpowers/plans/2026-05-18-selfhosted-licensing-sdk.md @@ -0,0 +1,575 @@ +# 自研授权 SDK 实施计划(完整版) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 实现自研软件授权 SDK(Rust native + Java SDK + 平台签发后端),与比特安索双线共存,Provider 可扩展架构。 + +**Architecture:** 单 Rust cdylib(craftlabs_auth_core)通过 Provider trait 路由。许可证 AES-256-GCM 加密载荷 + RSA-256 签名。签发走 delivery-platform-api:8080,SDK 在线交互走 license-webhook-ingress:8081。 + +**Tech Stack:** Rust (craft-core, cdylib/staticlib), Java 17 (Maven), Spring Boot 3.4.x, PostgreSQL 15 + MyBatis-Plus, Flyway + +**Reference Spec:** `docs/superpowers/specs/2026-05-18-selfhosted-licensing-sdk-design.md` + +--- + +## Phase 0-1 速览(Phase 1 完整步骤见下方任务列表) + +Phase 0(构建准备)+ Phase 1(离线核心:Rust crypto/device/cache/license 模块 + trait_provider + lib.rs 重构 + Java 配置扩展 + Schema + 数据库迁移 + LicenseSigner + LicenseController)共计 15 个任务。 + +--- + +## Task 0.1: 更新 Cargo.toml + +**Files:** Modify `native/craft-core/Cargo.toml` + +- [ ] **Step 1: 重命名 lib + 添加依赖** + +```toml +[lib] +crate-type = ["cdylib", "staticlib"] +name = "craftlabs_auth_core" + +[dependencies] +obfstr = "0.4" +sha2 = "0.10" +libloading = "0.8" +once_cell = "1" +rsa = { version = "0.9", features = ["sha2"] } +aes-gcm = "0.10" +hkdf = "0.12" +base64 = "0.22" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = { version = "0.4", features = ["serde"] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52", features = ["Win32_System_Diagnostics_Debug"] } + +[build-dependencies] +sha2 = "0.10" + +[features] +default = ["security-hardening"] +security-hardening = [] +``` + +- [ ] **Step 2: `cargo check --manifest-path native/craft-core/Cargo.toml`** +- [ ] **Step 3: `git commit -m "build(native): rename lib to craftlabs_auth_core, add selfhosted deps"`** + +--- + +## Task 1.1-1.6: Rust 离线核心模块(Part 1 覆盖) + +以下 6 个任务的完整代码见 Part 1(文档长度原因拆分)。Part 1 commit 记录: + +| 任务 | 文件 | 内容 | +|------|------|------| +| 1.1 | `error.rs` | 扩展错误码(加密/签名/许可证状态) | +| 1.2 | `crypto.rs` | HKDF 密钥派生 + AES-256-GCM 加解密 + RSA-SHA256 验签 | +| 1.3 | `device.rs` | 4 层硬件指纹采集(Linux DMI/machine-id/FS/MAC),stability_score | +| 1.4 | `provider_selfhosted/cache.rs` | 本地加密缓存(AES 加密写入磁盘 + 心跳 checkpoint) | +| 1.5 | `provider_selfhosted/license.rs` | 验签→解密→时间校验→离线宽限期 | +| 1.6 | `provider_selfhosted/mod.rs` | SelfHostedProvider struct + 初始化/离线校验/hasFeature | + +> Part 1 详细步骤已写入 Git:commit `d7469af`(spec)及之前。 + +--- + +## Task 1.7: trait_provider.rs(Provider trait + 路由) + +**Files:** Create `native/craft-core/src/trait_provider.rs` + +- [ ] **Step 1: 创建 trait_provider.rs** + +```rust +use std::collections::HashMap; +use crate::{CraftContext, LicenseInfo, error::LicenseError}; + +pub struct LicenseStatus { + pub licensed: bool, + pub not_after: Option, + pub features: HashMap, + pub device_count: u32, + pub max_devices: u32, + pub heartbeat_due: Option, +} + +pub struct ActivateResponse { + pub success: bool, + pub device_id: String, + pub license_payload: Vec, +} + +pub struct HeartbeatResponse { + pub valid: bool, + pub lease_until: Option, + pub update_available: bool, + pub new_license_payload: Option>, +} + +pub trait Provider: Send + Sync { + fn initialize(&mut self, ctx: &CraftContext, config_json: &str) -> Result<(), LicenseError>; + fn activate(&self, ctx: &CraftContext, license_key: &str) -> Result; + fn check_license(&self, ctx: &CraftContext) -> Result; + fn heartbeat(&self, ctx: &CraftContext) -> Result; + fn has_feature(&self, ctx: &CraftContext, name: &str) -> bool; + fn release(&mut self, ctx: &CraftContext) -> Result<(), LicenseError>; + fn get_license_info(&self, ctx: &CraftContext) -> LicenseInfo; + fn close(&mut self); +} +``` + +- [ ] **Step 2: `cargo check` → `git commit -m "feat(rust): add Provider trait abstraction"`** + +--- + +## Task 1.8: 重构 lib.rs(C ABI 路由到 Provider trait) + +**Files:** Modify `native/craft-core/src/lib.rs` + +- [ ] **Step 1: 替换 lib.rs** + +```rust +use std::ffi::CStr; +use std::os::raw::c_char; +use std::ptr; + +mod trait_provider; +mod error; +mod crypto; +mod device; +mod provider_selfhosted; +mod security; +mod session; + +pub use trait_provider::{Provider, ActivateResponse, HeartbeatResponse, LicenseStatus}; +pub use error::LicenseError; + +pub struct CraftContext { + pub provider: Option>, + pub initialized: bool, +} + +#[repr(C)] pub struct CraftResult { pub success: i32, pub message: *const c_char } + +#[repr(C)] +pub struct LicenseInfo { + pub is_licensed: i32, + pub expiration_date: i64, + pub feature_names: *const *const c_char, + pub feature_values: *const i32, + pub feature_count: i32, +} + +impl CraftContext { pub fn new() -> Self { CraftContext { provider: None, initialized: false } } } + +unsafe fn c_str_to_string(ptr: *const c_char) -> String { + if ptr.is_null() { String::new() } else { CStr::from_ptr(ptr).to_string_lossy().into_owned() } +} + +static OK_MSG: &[u8] = b"ok\0"; +fn ok_result() -> CraftResult { CraftResult { success: 1, message: OK_MSG.as_ptr() as *const c_char } } +fn craft_fail() -> CraftResult { + static FAIL: &[u8] = b"failure\0"; + CraftResult { success: 0, message: FAIL.as_ptr() as *const c_char } +} + +fn parse_provider(config: &str) -> String { + serde_json::from_str::(config).ok() + .and_then(|v| v.get("provider").and_then(|p| p.as_str().map(|s| s.to_string()))) + .unwrap_or_else(|| "selfhosted".to_string()) +} + +#[no_mangle] +pub extern "C" fn craft_initialize(config_json: *const c_char) -> *mut CraftContext { + let config_str = unsafe { c_str_to_string(config_json) }; + let mut ctx = Box::new(CraftContext::new()); + let mut provider: Box = match parse_provider(&config_str).as_str() { + "selfhosted" => Box::new(provider_selfhosted::SelfHostedProvider::new()), + _ => Box::new(provider_selfhosted::SelfHostedProvider::new()), + }; + if let Err(_) = provider.initialize(&ctx, &config_str) { + ctx.initialized = false; + } else { + ctx.provider = Some(provider); + ctx.initialized = true; + } + Box::into_raw(ctx) +} + +#[no_mangle] +pub extern "C" fn craft_activate(handle: *mut CraftContext, license_key: *const c_char, _: *const c_char) -> CraftResult { + if handle.is_null() { return craft_fail(); } + let ctx = unsafe { &*handle }; + let key = unsafe { c_str_to_string(license_key) }; + ctx.provider.as_ref().and_then(|p| p.activate(ctx, &key).ok()).map_or_else(craft_fail, |_| ok_result()) +} + +#[no_mangle] +pub extern "C" fn craft_check_license(handle: *mut CraftContext) -> CraftResult { + if handle.is_null() { return craft_fail(); } + let ctx = unsafe { &*handle }; + ctx.provider.as_ref().and_then(|p| p.check_license(ctx).ok()) + .map_or_else(craft_fail, |s| if s.licensed { ok_result() } else { craft_fail() }) +} + +#[no_mangle] +pub extern "C" fn craft_get_license_info(handle: *mut CraftContext) -> *mut LicenseInfo { + if handle.is_null() { return ptr::null_mut(); } + let ctx = unsafe { &*handle }; + ctx.provider.as_ref().map(|p| p.get_license_info(ctx)) + .map_or(ptr::null_mut(), |info| Box::into_raw(Box::new(info))) +} + +#[no_mangle] pub extern "C" fn craft_free_license_info(info: *mut LicenseInfo) { + if !info.is_null() { unsafe { drop(Box::from_raw(info)); } } +} + +#[no_mangle] +pub extern "C" fn craft_has_feature(handle: *mut CraftContext, feature_name: *const c_char) -> i32 { + if handle.is_null() { return 0; } + let ctx = unsafe { &*handle }; + let name = unsafe { c_str_to_string(feature_name) }; + ctx.provider.as_ref().map_or(0, |p| if p.has_feature(ctx, &name) { 1 } else { 0 }) +} + +#[no_mangle] pub extern "C" fn craft_release(handle: *mut CraftContext) -> CraftResult { + if handle.is_null() { return craft_fail(); } + let ctx = unsafe { &mut *handle }; + ctx.provider.as_mut().and_then(|p| p.release(ctx).ok()); + ok_result() +} + +#[no_mangle] pub extern "C" fn craft_heartbeat(handle: *mut CraftContext) -> CraftResult { + if handle.is_null() { return craft_fail(); } + let ctx = unsafe { &*handle }; + ctx.provider.as_ref().and_then(|p| p.heartbeat(ctx).ok()).map_or_else(craft_fail, |_| ok_result()) +} + +#[no_mangle] +pub extern "C" fn craft_destroy(handle: *mut CraftContext) { + if !handle.is_null() { + unsafe { let ctx = &mut *handle; if let Some(ref mut p) = ctx.provider { p.close(); } drop(Box::from_raw(handle)); } + } +} +``` + +- [ ] **Step 2: 在 provider_selfhosted/mod.rs 追加 Provider trait 实现** + +```rust +use crate::trait_provider::{Provider, ActivateResponse, HeartbeatResponse, LicenseStatus}; + +impl Provider for SelfHostedProvider { + fn initialize(&mut self, _ctx: &CraftContext, config_json: &str) -> Result<(), LicenseError> { + let cfg: serde_json::Value = serde_json::from_str(config_json).map_err(|_| LicenseError::InvalidFormat("config"))?; + let sh = cfg.get("selfhosted").ok_or(LicenseError::ConfigMissing("selfhosted"))?; + let base_url = sh.get("baseUrl").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let tenant_key = sh.get("tenantKey").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let ogd = sh.get("offlineGraceDays").and_then(|v| v.as_u64()).unwrap_or(7) as u32; + let hih = sh.get("heartbeatIntervalHours").and_then(|v| v.as_u64()).unwrap_or(24) as u32; + let pubkey = option_env!("CRAFTLABS_SELFHOSTED_PUBKEY").unwrap_or("").to_string(); + self.initialize(base_url, tenant_key, ogd, hih, pubkey) + } + fn activate(&self, _ctx: &CraftContext, _lk: &str) -> Result { + Err(LicenseError::Network("online activation not yet implemented".into())) + } + fn check_license(&self, _ctx: &CraftContext) -> Result { + self.check_license_offline()?; + let c = self.cache.license.as_ref().ok_or(LicenseError::NoCachedLicense)?; + Ok(LicenseStatus { licensed: true, not_after: c.not_after, features: c.features.clone(), device_count: 0, max_devices: c.max_devices, heartbeat_due: None }) + } + fn heartbeat(&self, _ctx: &CraftContext) -> Result { + Err(LicenseError::Network("heartbeat not yet implemented".into())) + } + fn has_feature(&self, _ctx: &CraftContext, name: &str) -> bool { self.has_feature_offline(name) } + fn release(&mut self, _ctx: &CraftContext) -> Result<(), LicenseError> { Ok(()) } + fn get_license_info(&self, _ctx: &CraftContext) -> LicenseInfo { self.get_license_info_offline() } + fn close(&mut self) { let _ = self.persist_cache(); } +} +``` + +- [ ] **Step 3: `cargo check` + `cargo test` → `git commit -m "feat(rust): refactor C ABI to route through Provider trait"`** + +--- + +## Task 1.9-1.10: Java 配置 + Schema 扩展 + +**Files:** Modify `SelfhostedConfigSection.java`, `FeatureMapping.java`, `schemas/craftlabs-auth-config.schema.json`, `examples/config/school.selfhosted.json` + +- [ ] **Step 1: SelfhostedConfigSection 新增字段** + +```java +public record SelfhostedConfigSection( + @JsonProperty("baseUrl") String baseUrl, + @JsonProperty("tenantKey") String tenantKey, + @JsonProperty("offlineGraceDays") Integer offlineGraceDays, // ★ 新增 + @JsonProperty("heartbeatIntervalHours") Integer heartbeatIntervalHours, // ★ 新增 + @JsonProperty("publicKeyPem") String publicKeyPem) { // ★ 新增 + public SelfhostedConfigSection { + offlineGraceDays = offlineGraceDays != null ? offlineGraceDays : 7; + heartbeatIntervalHours = heartbeatIntervalHours != null ? heartbeatIntervalHours : 24; + } +} +``` + +- [ ] **Step 2: FeatureMapping 新增 selfhostedFeatureKey** + +```java +public record FeatureMapping( + @JsonProperty("bitanswerFeatureId") Integer bitanswerFeatureId, + @JsonProperty("bitanswerFeatureName") String bitanswerFeatureName, + @JsonProperty("selfhostedFeatureKey") String selfhostedFeatureKey) {} // ★ 新增 +``` + +- [ ] **Step 3: Schema 扩展 selfhosted properties** + +```json +"offlineGraceDays": { "type": "integer", "minimum": 0, "maximum": 365, "default": 7 }, +"heartbeatIntervalHours": { "type": "integer", "minimum": 1, "maximum": 720, "default": 24 }, +"publicKeyPem": { "type": "string" } +``` + +同步扩展 `features.additionalProperties` 增加 `selfhostedFeatureKey`。 + +- [ ] **Step 4: 更新 `examples/config/school.selfhosted.json`** +- [ ] **Step 5: `mvn compile -pl craftlabs-auth-core` + Schema 校验 → `git commit -m "feat: extend Java config and Schema for selfhosted SDK"`** + +--- + +## Task 1.11: 平台数据库迁移(Flyway V2) + +**Files:** Create `services/delivery-platform-api/src/main/resources/db/migration/V2__selfhosted_licensing.sql` + +核心 6 张表:`platform_license_policies`, `platform_license_keys`, `platform_licenses`, `platform_license_features`, `platform_license_activations`, `platform_license_heartbeats`。完整 DDL 见设计文档 §3.6。 + +- [ ] **Step: `docker compose up -d postgres` + `mvn flyway:migrate` → commit** + +--- + +## Task 1.12: LicenseSigner.java(RSA 签名 + AES 加密签发) + +**Files:** Create `services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/license/LicenseSigner.java` + +```java +@Service +public class LicenseSigner { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String EMBEDDED_SALT = "craftlabs-license-salt-v1-2026-05"; + + public String sign(SignedLicensePayload payload, PlatformLicenseKey key) throws Exception { + byte[] payloadJson = MAPPER.writeValueAsBytes(payload); + byte[] aesKey = deriveAesKey(EMBEDDED_SALT, payload.getLicenseId()); + byte[] encrypted = aesGcmEncrypt(aesKey, payloadJson); + String payloadB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(encrypted); + + PrivateKey privateKey = loadPrivateKey(key.getPrivateKey()); + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initSign(privateKey); + sig.update(encrypted); + String sigB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(sig.sign()); + + LicenseDocument doc = LicenseDocument.builder() + .version(1).licenseId(payload.getLicenseId()) + .payload(payloadB64) + .signature(SignatureBlock.builder().algorithm("RS256").keyId(key.getKeyId()).value(sigB64).build()) + .build(); + return MAPPER.writeValueAsString(doc); + } + + private byte[] deriveAesKey(String salt, String licenseId) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(salt.getBytes("UTF-8"), "HmacSHA256")); + byte[] prk = mac.doFinal(new byte[0]); + mac.init(new SecretKeySpec(prk, "HmacSHA256")); + mac.update(licenseId.getBytes("UTF-8")); + mac.update((byte) 0x01); + return mac.doFinal(); + } + + private byte[] aesGcmEncrypt(byte[] key, byte[] plaintext) throws Exception { + byte[] nonce = new byte[12]; new SecureRandom().nextBytes(nonce); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce)); + byte[] ct = cipher.doFinal(plaintext); + byte[] r = new byte[12 + ct.length]; System.arraycopy(nonce, 0, r, 0, 12); System.arraycopy(ct, 0, r, 12, ct.length); + return r; + } +} +``` + +- [ ] **Step: `mvn compile` → `git commit -m "feat(platform): add LicenseSigner for RSA+AES license issuance"`** + +--- + +## Task 1.13-1.14: Entity/Mapper + LicenseService + LicenseController + +**Files:** Create 5 组 Entity+Mapper(`persistence/license/`)+ LicenseService + LicenseController + +核心 API: + +``` +POST /api/v1/licenses → 签发许可证(需 LICENSE_OPS 角色) +GET /api/v1/licenses/{id} → 查询详情 +POST /api/v1/licenses/{id}/revoke → 吊销 +``` + +SecurityConfig 新增:`.requestMatchers("/api/v1/licenses/**").hasAnyRole("LICENSE_OPS", "ADMIN")` + +- [ ] **Step: `mvn compile` + `mvn test -Dtest=LicenseControllerTest` → commit** + +--- + +## Task 1.15: Phase 1 集成验证 + +- [ ] **Step 1: 生成 RSA 密钥对** + +```bash +openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 +openssl rsa -pubout -in private_key.pem -out public_key.pem +``` + +- [ ] **Step 2: `export CRAFTLABS_SELFHOSTED_PUBKEY="$(cat public_key.pem)"`** +- [ ] **Step 3: `cargo test --manifest-path native/craft-core/Cargo.toml` → all pass** +- [ ] **Step 4: 启动服务 + curl 签发 → Rust 离线验签通过** + +```bash +curl -X POST http://localhost:8080/api/v1/licenses \ + -H "Authorization: Bearer $TOKEN" -d '{"tenantId":"test",...}' +# 将返回的 license.json 保存 → Rust 集成测 validate +``` + +- [ ] **Step 5: Commit** + +--- + +## Phase 2: 在线激活(3 个任务) + +### Task 2.1: protocol.rs(HTTP 请求/响应序列化) + +**Files:** Create `native/craft-core/src/provider_selfhosted/protocol.rs` + +Serialize/Deserialize DTOs:`ActivateRequest`, `ActivateResponseBody`, `HeartbeatRequest/Response`, `CheckRequest/Response`, `ReleaseRequest/Response`。HMAC 签名工具函数 `build_hmac_signature`。 + +新增依赖:`reqwest = "0.12"`, `hex = "0.4"`, `hmac = "0.12"`, `rand = "0.8"`, `tokio = "1"` + +- [ ] **Step: `cargo check` → commit** + +### Task 2.2: activate.rs(在线激活 HTTPS 请求) + +**Files:** Create `native/craft-core/src/provider_selfhosted/activate.rs` + +```rust +pub async fn online_activate(config: &SelfHostedConfig, fp: &DeviceFingerprint, license_key: &str) + -> Result +{ + let req = protocol::ActivateRequest { license_key: license_key.to_string(), device_fingerprint: /* fp */ }; + let body = serde_json::to_string(&req).unwrap(); + // POST /license/v1/activate + HMAC headers ... + match resp.status() { + 200 => Ok(parse_body), + 409 => Err(DeviceLimitReached), + 403 => Err(LicenseRevoked), + } +} +``` + +在 `provider_selfhosted/mod.rs` 的 `activate()` 中调用 `tokio::runtime::Handle::current().block_on(online_activate(...))`。 + +- [ ] **Step: `cargo test` → commit** + +### Task 2.3: Webhook 侧 LicenseController + ActivateService + +**Files:** Create license endpoints in `services/license-webhook-ingress` + +- `NonceValidator.java`: Nonce 去重(ConcurrentHashMap + 5min 窗口) +- `LicenseActivateService.java`: 查许可证状态 + 终端配额 + 设备匹配 + 下发生效 +- `LicenseController.java`: `POST /license/v1/activate`(Bearer tenantKey + HMAC header 校验) + +- [ ] **Step: `mvn compile` + `mvn test` → commit** + +--- + +## Phase 3: 心跳 + 离线兜底(2 个任务) + +### Task 3.1: heartbeat.rs(在线心跳 + 吊销检测) + +**Files:** Create `native/craft-core/src/provider_selfhosted/heartbeat.rs` + +```rust +pub async fn online_heartbeat(config: &SelfHostedConfig, device_hash: &str, license_key: &str) + -> Result +{ + // POST /license/v1/heartbeat → + // 200: 更新租约 + 可选下发新许可证 + // 410: LicenseRevoked +} +``` + +更新 mod.rs heartbeat() 实现。 + +### Task 3.2: Webhook HeartbeatService + CheckService + ReleaseService + +**Files:** Append to webhook `LicenseController` + +- `POST /license/v1/heartbeat` — 更新 `last_heartbeat`,返回租约续期时间 +- `POST /license/v1/check` — 在线校验许可证有效性 +- `POST /license/v1/release` — 释放终端占用 + +--- + +## Phase 4: 完善与生产加固(5 个任务) + +### Task 4.1: build.rs 嵌入 RSA 公钥 + +**Files:** Modify `native/craft-core/build.rs`, Create `native/craft-core/embedded/pubkey.pem` + +```rust +// build.rs +fn main() { + let pubkey = fs::read_to_string("embedded/pubkey.pem").unwrap_or_default(); + println!("cargo:rustc-env=CRAFTLABS_SELFHOSTED_PUBKEY={}", pubkey.trim()); + println!("cargo:rerun-if-changed=embedded/pubkey.pem"); +} +``` + +将生产公钥放入 `embedded/pubkey.pem`(不提交私钥)。 + +### Task 4.2: SelfHostedAuthProvider 加载正确库 + +**Files:** Modify `java/craftlabs-auth-selfhosted/.../SelfHostedAuthProvider.java` + +改 `System.loadLibrary("craftlabs_auth_bitanswer")` → `System.loadLibrary("craftlabs_auth_core")`。 + +### Task 4.3: MultiProviderSmokeTest + +**Files:** Create `java/craftlabs-auth-tests/.../MultiProviderSmokeTest.java` + +测试:初始化 selfhosted → 离线校验 → close → 初始化 bitanswer → close,双线不冲突。 + +### Task 4.4: CI 适配 + +- `ci-native.yml`: 更新 artifact name,确保 `embedded/pubkey.pem` 存在 +- `ci-platform.yml`: 新增 license 模块测试 Job + +### Task 4.5: 最终集成验证 + +```bash +cargo test --manifest-path native/craft-core/Cargo.toml # Rust 全量 +mvn -f java/pom.xml verify # Java SDK +mvn -f services/pom.xml verify # 平台 +docker compose -f services/docker-compose.yml up -d postgres # 起库 +# curl 签发 → Rust 离线验签 → curl 在线激活 → 终端满 409 → 吊销 410 +``` + +--- + +## 总结 + +| Phase | 任务数 | 核心交付 | +|-------|--------|----------| +| P0 | 1 | Cargo.toml 依赖 | +| P1 | 14 | Rust 离线核心 + Java 配置 + 平台签发后端 | +| P2 | 3 | 在线激活(Rust HTTPS + Webhook 端点) | +| P3 | 2 | 心跳 + 离线兜底 + 吊销检测 | +| P4 | 5 | build.rs 嵌入 + 测试 + CI | +| **合计** | **25** | |