fix(secrets-mcp 0.5.20): code review plan — export secret types, env map, rollback, API key, MCP tools, web session & validation

- Export/import: optional secret_types map; AddResult includes entry_id
- env_map: dot→__ segment encoding; collision errors
- rollback: FOR UPDATE + txn-consistent snapshot; restore name from history
- regenerate_api_key: rows_affected guard
- MCP: find count propagates errors; add uses entry_id for relations; rollback no encryption key
- Web: load_session_user_strict + JSON handlers key_version; PATCH length limits
- Tests: ExportEntry serde, env segment
This commit is contained in:
voson
2026-04-11 17:10:16 +08:00
parent 2c7dbf890b
commit d772066210
13 changed files with 266 additions and 141 deletions

View File

@@ -30,58 +30,61 @@ pub async fn run(
folder: String,
#[sqlx(rename = "type")]
entry_type: String,
name: String,
version: i64,
action: String,
tags: Vec<String>,
metadata: Value,
}
let live_entry: Option<EntryWriteRow> = if let Some(uid) = user_id {
let mut tx = pool.begin().await?;
let live: Option<EntryWriteRow> = if let Some(uid) = user_id {
sqlx::query_as(
"SELECT id, version, folder, type, name, tags, metadata, notes, deleted_at FROM entries \
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL",
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL FOR UPDATE",
)
.bind(entry_id)
.bind(uid)
.fetch_optional(pool)
.fetch_optional(&mut *tx)
.await?
} else {
sqlx::query_as(
"SELECT id, version, folder, type, name, tags, metadata, notes, deleted_at FROM entries \
WHERE id = $1 AND user_id IS NULL AND deleted_at IS NULL",
WHERE id = $1 AND user_id IS NULL AND deleted_at IS NULL FOR UPDATE",
)
.bind(entry_id)
.fetch_optional(pool)
.fetch_optional(&mut *tx)
.await?
};
let live_entry = live_entry.ok_or(AppError::NotFoundEntry)?;
let lr = live.ok_or(AppError::NotFoundEntry)?;
let snap: Option<EntryHistoryRow> = if let Some(ver) = to_version {
sqlx::query_as(
"SELECT folder, type, version, action, tags, metadata \
"SELECT folder, type, name, version, action, tags, metadata \
FROM entries_history \
WHERE entry_id = $1 AND version = $2 ORDER BY id ASC LIMIT 1",
)
.bind(entry_id)
.bind(ver)
.fetch_optional(pool)
.fetch_optional(&mut *tx)
.await?
} else {
sqlx::query_as(
"SELECT folder, type, version, action, tags, metadata \
"SELECT folder, type, name, version, action, tags, metadata \
FROM entries_history \
WHERE entry_id = $1 ORDER BY id DESC LIMIT 1",
)
.bind(entry_id)
.fetch_optional(pool)
.fetch_optional(&mut *tx)
.await?
};
let snap = snap.ok_or_else(|| {
anyhow::anyhow!(
"No history found for entry '{}'{}.",
live_entry.name,
lr.name,
to_version
.map(|v| format!(" at version {}", v))
.unwrap_or_default()
@@ -91,17 +94,7 @@ pub async fn run(
let snap_secret_snapshot = db::entry_secret_snapshot_from_metadata(&snap.metadata);
let snap_metadata = db::strip_secret_snapshot_from_metadata(&snap.metadata);
let mut tx = pool.begin().await?;
let live: Option<EntryWriteRow> = sqlx::query_as(
"SELECT id, version, folder, type, name, tags, metadata, notes, deleted_at FROM entries \
WHERE id = $1 AND deleted_at IS NULL FOR UPDATE",
)
.bind(entry_id)
.fetch_optional(&mut *tx)
.await?;
let live_entry_id = if let Some(ref lr) = live {
let live_entry_id = {
let history_metadata =
match db::metadata_with_secret_snapshot(&mut tx, lr.id, &lr.metadata).await {
Ok(v) => v,
@@ -168,8 +161,8 @@ pub async fn run(
)
.bind(&snap.folder)
.bind(&snap.entry_type)
.bind(&live_entry.name)
.bind(&live_entry.notes)
.bind(&snap.name)
.bind(&lr.notes)
.bind(&snap.tags)
.bind(&snap_metadata)
.bind(lr.id)
@@ -177,8 +170,6 @@ pub async fn run(
.await?;
lr.id
} else {
return Err(AppError::NotFoundEntry.into());
};
if let Some(secret_snapshot) = snap_secret_snapshot {
@@ -191,7 +182,7 @@ pub async fn run(
"rollback",
&snap.folder,
&snap.entry_type,
&live_entry.name,
&snap.name,
serde_json::json!({
"entry_id": entry_id,
"restored_version": snap.version,
@@ -203,7 +194,7 @@ pub async fn run(
tx.commit().await?;
Ok(RollbackResult {
name: live_entry.name,
name: snap.name,
folder: snap.folder,
entry_type: snap.entry_type,
restored_version: snap.version,