release(secrets-mcp): 0.5.10 — Web 模块化、性能与错误处理
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 6m3s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 1m36s

- 拆分 web.rs 为 web/ 子模块;统一 client_ip 提取
- core: user_scope SQL 复用、env_map N+1 消除、FETCH_ALL 上限调整
- entries 列表页并行查询;PgPool 去 Arc;结构化 NotFound 等错误
- CI: SSH 私钥安全写入;crypto/hex 与依赖清理;MCP 输入长度校验
- AGENTS: API Key 明文存储设计说明
This commit is contained in:
voson
2026-04-06 18:01:44 +08:00
parent b0fcb83592
commit 59084a409d
28 changed files with 2300 additions and 2254 deletions

View File

@@ -5,6 +5,7 @@ use uuid::Uuid;
use crate::db;
use crate::models::{EntryRow, EntryWriteRow, SecretFieldRow};
use crate::service::util::user_scope_condition;
#[derive(Debug, serde::Serialize)]
pub struct DeletedEntry {
@@ -126,48 +127,32 @@ async fn delete_one(
// - 2+ matches → disambiguation error (same as non-dry-run)
#[derive(sqlx::FromRow)]
struct DryRunRow {
#[allow(dead_code)]
id: Uuid,
folder: String,
#[sqlx(rename = "type")]
entry_type: String,
}
let rows: Vec<DryRunRow> = if let Some(uid) = user_id {
if let Some(f) = folder {
sqlx::query_as(
"SELECT id, folder, type FROM entries WHERE user_id = $1 AND folder = $2 AND name = $3",
)
.bind(uid)
.bind(f)
.bind(name)
.fetch_all(pool)
.await?
} else {
sqlx::query_as(
"SELECT id, folder, type FROM entries WHERE user_id = $1 AND name = $2",
)
.bind(uid)
.bind(name)
.fetch_all(pool)
.await?
}
} else if let Some(f) = folder {
sqlx::query_as(
"SELECT id, folder, type FROM entries WHERE user_id IS NULL AND folder = $1 AND name = $2",
)
.bind(f)
.bind(name)
.fetch_all(pool)
.await?
} else {
sqlx::query_as(
"SELECT id, folder, type FROM entries WHERE user_id IS NULL AND name = $1",
)
.bind(name)
.fetch_all(pool)
.await?
};
let mut idx = 1i32;
let user_cond = user_scope_condition(user_id, &mut idx);
let mut conditions = vec![user_cond];
if folder.is_some() {
conditions.push(format!("folder = ${}", idx));
idx += 1;
}
conditions.push(format!("name = ${}", idx));
let sql = format!(
"SELECT folder, type FROM entries WHERE {}",
conditions.join(" AND ")
);
let mut q = sqlx::query_as::<_, DryRunRow>(&sql);
if let Some(uid) = user_id {
q = q.bind(uid);
}
if let Some(f) = folder {
q = q.bind(f);
}
q = q.bind(name);
let rows = q.fetch_all(pool).await?;
return match rows.len() {
0 => Ok(DeleteResult {
@@ -175,7 +160,10 @@ async fn delete_one(
dry_run: true,
}),
1 => {
let row = rows.into_iter().next().unwrap();
let row = rows
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("internal: matched row vanished"))?;
Ok(DeleteResult {
deleted: vec![DeletedEntry {
name: name.to_string(),
@@ -201,45 +189,27 @@ async fn delete_one(
let mut tx = pool.begin().await?;
// Fetch matching rows with FOR UPDATE; use folder when provided to resolve ambiguity.
let rows: Vec<EntryRow> = if let Some(uid) = user_id {
if let Some(f) = folder {
sqlx::query_as(
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
WHERE user_id = $1 AND folder = $2 AND name = $3 FOR UPDATE",
)
.bind(uid)
.bind(f)
.bind(name)
.fetch_all(&mut *tx)
.await?
} else {
sqlx::query_as(
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
WHERE user_id = $1 AND name = $2 FOR UPDATE",
)
.bind(uid)
.bind(name)
.fetch_all(&mut *tx)
.await?
}
} else if let Some(f) = folder {
sqlx::query_as(
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
WHERE user_id IS NULL AND folder = $1 AND name = $2 FOR UPDATE",
)
.bind(f)
.bind(name)
.fetch_all(&mut *tx)
.await?
} else {
sqlx::query_as(
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
WHERE user_id IS NULL AND name = $1 FOR UPDATE",
)
.bind(name)
.fetch_all(&mut *tx)
.await?
};
let mut idx = 1i32;
let user_cond = user_scope_condition(user_id, &mut idx);
let mut conditions = vec![user_cond];
if folder.is_some() {
conditions.push(format!("folder = ${}", idx));
idx += 1;
}
conditions.push(format!("name = ${}", idx));
let sql = format!(
"SELECT id, version, folder, type, tags, metadata, notes FROM entries WHERE {} FOR UPDATE",
conditions.join(" AND ")
);
let mut q = sqlx::query_as::<_, EntryRow>(&sql);
if let Some(uid) = user_id {
q = q.bind(uid);
}
if let Some(f) = folder {
q = q.bind(f);
}
q = q.bind(name);
let rows = q.fetch_all(&mut *tx).await?;
let row = match rows.len() {
0 => {
@@ -249,7 +219,10 @@ async fn delete_one(
dry_run: false,
});
}
1 => rows.into_iter().next().unwrap(),
1 => rows
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("internal: matched row vanished"))?,
_ => {
tx.rollback().await?;
let folders: Vec<&str> = rows.iter().map(|r| r.folder.as_str()).collect();