Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ anyhow = "1"
base64 = "0.22"
directories = "6"
fs2 = "0.4"
hex = "0.4"
reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls", "system-proxy"], default-features = false }
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tempfile = "3"
thiserror = "2"
tokio = { version = "1", features = ["macros", "process", "rt-multi-thread", "time"] }
Expand Down
22 changes: 21 additions & 1 deletion apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -523,9 +523,15 @@ export function App() {
});
const [removeOwnedData, setRemoveOwnedData] = useState(false);

const call = <T,>(command: string, args?: Record<string, unknown>) => invoke<T>(command, args);
const call = <T,>(command: string, args?: Record<string, unknown>): Promise<T> => {
if (!tauriRuntimeAvailable()) {
return Promise.reject(new Error(TAURI_RUNTIME_UNAVAILABLE_MESSAGE));
}
return invoke<T>(command, args);
};

const logDiagnostic = (event: string, detail: Record<string, unknown> = {}) => {
if (!tauriRuntimeAvailable()) return;
void invoke("write_diagnostic_event", { event, detail }).catch(() => {});
};

Expand Down Expand Up @@ -4876,6 +4882,20 @@ function stringifyError(error: unknown) {
return String(error);
}

// Tauri v2 注入 window.__TAURI_INTERNALS__;缺失时说明当前不在管理工具窗口里
// (例如用浏览器打开了 dev server,或在 devtools/自动化环境中执行),
// 此时 @tauri-apps/api 的 invoke 会抛 "Cannot read properties of undefined (reading 'invoke')"。
function tauriRuntimeAvailable(): boolean {
return (
typeof window !== "undefined" &&
typeof (window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ !== "undefined"
);
}

const TAURI_RUNTIME_UNAVAILABLE_MESSAGE =
"Tauri 运行时不可用:请在已安装的「Codex++ 管理工具」窗口中操作。" +
"当前页面可能是在浏览器或开发模式 (tauri dev) 下打开的,无法调用后端命令。";

function loadInitialTheme(): Theme {
if (typeof window === "undefined") return "dark";
return window.localStorage.getItem("codex-plus-theme") === "light" ? "light" : "dark";
Expand Down
2 changes: 2 additions & 0 deletions crates/codex-plus-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ async-trait = "0.1"
base64.workspace = true
directories.workspace = true
futures-util = "0.3"
hex.workspace = true
reqwest.workspace = true
rusqlite.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["net"] }
tokio-tungstenite.workspace = true
Expand Down
24 changes: 24 additions & 0 deletions crates/codex-plus-core/src/script_market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ pub fn install_market_script_content(
script: &MarketScript,
content: &[u8],
) -> anyhow::Result<()> {
// 先校验完整性再落盘:市场脚本是注入页面执行的 JS,内容哈希是其唯一防篡改手段。
// 校验失败立即返回,不写文件、不记录安装,保留已有版本不变。
verify_script_checksum(script, content)?;
let path = manager.user_script_path_for_market_id(&script.id);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
Expand All @@ -108,6 +111,27 @@ pub async fn install_market_script(
install_market_script_content(manager, script, &content)
}

/// 比对下载内容与清单声明的 sha256。
///
/// 清单未提供 sha256(字段为空)时跳过校验以兼容无哈希的旧清单;提供时按
/// 大小写不敏感的十六进制比对,不匹配即返回错误。
fn verify_script_checksum(script: &MarketScript, content: &[u8]) -> anyhow::Result<()> {
use sha2::{Digest, Sha256};

let expected = script.sha256.trim();
if expected.is_empty() {
return Ok(());
}
let actual = hex::encode(Sha256::digest(content));
if !actual.eq_ignore_ascii_case(expected) {
anyhow::bail!(
"脚本 {} 的 sha256 校验失败:清单声明 {expected},实际下载内容为 {actual}",
script.id
);
}
Ok(())
}

fn parse_market_script(raw: Value) -> Option<MarketScript> {
let id = required_string(&raw, "id")?;
let name = required_string(&raw, "name")?;
Expand Down
79 changes: 67 additions & 12 deletions crates/codex-plus-core/tests/bridge_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -743,8 +743,22 @@ fn install_market_script_writes_file_and_records_metadata() {
assert_eq!(inventory["scripts"][0]["market_id"], "demo");
}

fn market_script_with_sha256(sha256: &str) -> codex_plus_core::script_market::MarketScript {
codex_plus_core::script_market::MarketScript {
id: "demo".to_string(),
name: "Demo".to_string(),
description: String::new(),
version: "1.0.0".to_string(),
author: String::new(),
tags: Vec::new(),
homepage: String::new(),
script_url: "https://example.com/demo.js".to_string(),
sha256: sha256.to_string(),
}
}

#[test]
fn install_market_script_ignores_checksum_mismatch_and_replaces_existing_file() {
fn install_market_script_rejects_checksum_mismatch_and_keeps_existing_file() {
let temp = tempfile::tempdir().unwrap();
let user_dir = temp.path().join("user");
std::fs::create_dir_all(&user_dir).unwrap();
Expand All @@ -754,17 +768,58 @@ fn install_market_script_ignores_checksum_mismatch_and_replaces_existing_file()
user_dir.clone(),
temp.path().join("user_scripts.json"),
);
let script = codex_plus_core::script_market::MarketScript {
id: "demo".to_string(),
name: "Demo".to_string(),
description: String::new(),
version: "1.0.0".to_string(),
author: String::new(),
tags: Vec::new(),
homepage: String::new(),
script_url: "https://example.com/demo.js".to_string(),
sha256: "0000".to_string(),
};
let script = market_script_with_sha256("0000");

let result =
codex_plus_core::script_market::install_market_script_content(&manager, &script, b"new");

assert!(result.is_err(), "checksum mismatch must abort the install");
// 安装被拒,旧版本原样保留,未记录到配置。
assert_eq!(
std::fs::read_to_string(user_dir.join("market-demo.js")).unwrap(),
"old"
);
assert!(
!manager
.load_config()
.scripts
.contains_key("user:market-demo.js")
);
}

#[test]
fn install_market_script_accepts_matching_checksum() {
let temp = tempfile::tempdir().unwrap();
let user_dir = temp.path().join("user");
let manager = UserScriptManager::new(
temp.path().join("builtin"),
user_dir.clone(),
temp.path().join("user_scripts.json"),
);
// sha256("new")
let script = market_script_with_sha256(
"11507a0e2f5e69d5dfa40a62a1bd7b6ee57e6bcd85c67c9b8431b36fff21c437",
);

codex_plus_core::script_market::install_market_script_content(&manager, &script, b"new")
.unwrap();

assert_eq!(
std::fs::read_to_string(user_dir.join("market-demo.js")).unwrap(),
"new"
);
}

#[test]
fn install_market_script_skips_verification_when_checksum_empty() {
let temp = tempfile::tempdir().unwrap();
let user_dir = temp.path().join("user");
let manager = UserScriptManager::new(
temp.path().join("builtin"),
user_dir.clone(),
temp.path().join("user_scripts.json"),
);
let script = market_script_with_sha256("");

codex_plus_core::script_market::install_market_script_content(&manager, &script, b"new")
.unwrap();
Expand Down
Loading