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

@@ -184,6 +184,9 @@ pub struct ExportEntry {
/// Decrypted secret fields. None means no secrets in this export (--no-secrets).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secrets: Option<BTreeMap<String, Value>>,
/// Per-secret types (`text`, `password`, `key`, …). Omitted in legacy exports; importers default to `"text"`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secret_types: Option<BTreeMap<String, String>>,
}
// ── Multi-user models ──────────────────────────────────────────────────────────
@@ -311,3 +314,44 @@ pub fn toml_to_json_value(v: &toml::Value) -> Value {
}
}
}
#[cfg(test)]
mod export_entry_tests {
use super::*;
use std::collections::BTreeMap;
#[test]
fn export_entry_roundtrip_includes_secret_types() {
let mut secrets = BTreeMap::new();
secrets.insert("k".to_string(), serde_json::json!("v"));
let mut types = BTreeMap::new();
types.insert("k".to_string(), "password".to_string());
let e = ExportEntry {
name: "n".to_string(),
folder: "f".to_string(),
entry_type: "t".to_string(),
notes: "".to_string(),
tags: vec![],
metadata: serde_json::json!({}),
secrets: Some(secrets),
secret_types: Some(types),
};
let json = serde_json::to_string(&e).unwrap();
let back: ExportEntry = serde_json::from_str(&json).unwrap();
assert_eq!(
back.secret_types
.as_ref()
.unwrap()
.get("k")
.map(String::as_str),
Some("password")
);
}
#[test]
fn export_entry_legacy_json_without_secret_types_deserializes() {
let json = r#"{"name":"a","folder":"","type":"","notes":"","tags":[],"metadata":{},"secrets":{"x":"y"}}"#;
let e: ExportEntry = serde_json::from_str(json).unwrap();
assert!(e.secret_types.is_none());
}
}