From 5d615dd3936e0f51fe5ea027d0b5eb99f33e0061 Mon Sep 17 00:00:00 2001 From: huangping Date: Mon, 18 May 2026 22:39:05 +0800 Subject: [PATCH] feat: add build.rs pubkey embedding and webhook license/v1 endpoints --- native/craft-core/build.rs | 11 +++- native/craft-core/embedded/pubkey.pem | 1 + .../webhook/license/LicenseController.java | 61 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 native/craft-core/embedded/pubkey.pem create mode 100644 services/license-webhook-ingress/src/main/java/cn/craftlabs/platform/webhook/license/LicenseController.java diff --git a/native/craft-core/build.rs b/native/craft-core/build.rs index 97b3e3d..d150961 100644 --- a/native/craft-core/build.rs +++ b/native/craft-core/build.rs @@ -11,11 +11,20 @@ fn main() { hash_dir(&src_dir, &mut hasher); let digest = hasher.finalize(); let hash_hex = format!("{:x}", digest); - println!("cargo:rustc-env=BUILD_SRC_HASH={}", hash_hex); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); fs::write(out_dir.join("build_hash.txt"), format!("{}\n", hash_hex)).unwrap(); + + // 嵌入 RSA 公钥(用于 selfhosted 验签) + let pubkey_path = PathBuf::from(&manifest_dir).join("embedded").join("pubkey.pem"); + if let Ok(pubkey) = fs::read_to_string(&pubkey_path) { + let trimmed = pubkey.trim(); + if !trimmed.is_empty() { + println!("cargo:rustc-env=CRAFTLABS_SELFHOSTED_PUBKEY={}", trimmed); + } + } + println!("cargo:rerun-if-changed=embedded/pubkey.pem"); } fn hash_dir(dir: &PathBuf, hasher: &mut Sha256) { diff --git a/native/craft-core/embedded/pubkey.pem b/native/craft-core/embedded/pubkey.pem new file mode 100644 index 0000000..3a3113e --- /dev/null +++ b/native/craft-core/embedded/pubkey.pem @@ -0,0 +1 @@ +# Placeholder — replace with production RSA public key (PEM format) diff --git a/services/license-webhook-ingress/src/main/java/cn/craftlabs/platform/webhook/license/LicenseController.java b/services/license-webhook-ingress/src/main/java/cn/craftlabs/platform/webhook/license/LicenseController.java new file mode 100644 index 0000000..c9aba39 --- /dev/null +++ b/services/license-webhook-ingress/src/main/java/cn/craftlabs/platform/webhook/license/LicenseController.java @@ -0,0 +1,61 @@ +package cn.craftlabs.platform.webhook.license; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import java.util.Map; + +@RestController +@RequestMapping("/license/v1") +public class LicenseController { + + @PostMapping("/activate") + public ResponseEntity> activate( + @RequestHeader(value = "Authorization", required = false) String auth, + @RequestHeader(value = "X-Craft-Nonce", required = false) String nonce, + @RequestHeader(value = "X-Craft-Timestamp", required = false) String timestamp, + @RequestHeader(value = "X-Craft-Signature", required = false) String signature, + @RequestBody Map request) { + + // Phase 1 MVP: accept all activations + String licenseKey = (String) request.getOrDefault("license_key", "unknown"); + @SuppressWarnings("unchecked") + Map fp = (Map) request.get("device_fingerprint"); + String deviceHash = fp != null ? (String) fp.getOrDefault("composite_hash", "HC-unknown") : "HC-unknown"; + + Map response = new java.util.HashMap<>(); + response.put("status", "activated"); + response.put("device_id", deviceHash); + response.put("license_payload", "{\"placeholder\":true,\"note\":\"replace with real license JSON\"}"); + return ResponseEntity.ok(response); + } + + @PostMapping("/heartbeat") + public ResponseEntity> heartbeat( + @RequestHeader(value = "Authorization", required = false) String auth, + @RequestHeader(value = "X-Craft-Nonce", required = false) String nonce, + @RequestHeader(value = "X-Craft-Timestamp", required = false) String timestamp, + @RequestHeader(value = "X-Craft-Signature", required = false) String signature, + @RequestBody Map request) { + + Map response = new java.util.HashMap<>(); + response.put("status", "ok"); + response.put("lease_renewed_until", java.time.Instant.now().plus(java.time.Duration.ofHours(24)).toString()); + response.put("update_available", false); + return ResponseEntity.ok(response); + } + + @PostMapping("/check") + public ResponseEntity> check(@RequestBody Map request) { + Map response = new java.util.HashMap<>(); + response.put("status", "valid"); + response.put("not_after", java.time.Instant.now().plus(java.time.Duration.ofDays(365)).toString()); + return ResponseEntity.ok(response); + } + + @PostMapping("/release") + public ResponseEntity> release(@RequestBody Map request) { + Map response = new java.util.HashMap<>(); + response.put("status", "released"); + return ResponseEntity.ok(response); + } +}