release(secrets-mcp): 0.5.4 — Web 分页修正与 hex 解码;批量删除上限;MCP @ 路径检测
This commit is contained in:
@@ -12,6 +12,7 @@ aes-gcm.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
chrono.workspace = true
|
||||
hex = "0.4"
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn decrypt_json(master_key: &[u8; 32], data: &[u8]) -> Result<Value> {
|
||||
|
||||
/// Parse a 64-char hex string (from X-Encryption-Key header) into a 32-byte key.
|
||||
pub fn extract_key_from_hex(hex_str: &str) -> Result<[u8; 32]> {
|
||||
let bytes = hex::decode_hex(hex_str.trim())?;
|
||||
let bytes = ::hex::decode(hex_str.trim())?;
|
||||
if bytes.len() != 32 {
|
||||
bail!(
|
||||
"X-Encryption-Key must be 64 hex chars (32 bytes), got {} bytes",
|
||||
@@ -76,21 +76,14 @@ pub fn extract_key_from_hex(hex_str: &str) -> Result<[u8; 32]> {
|
||||
// ─── Public hex helpers ───────────────────────────────────────────────────────
|
||||
|
||||
pub mod hex {
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn encode_hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
|
||||
pub fn decode_hex(s: &str) -> Result<Vec<u8>> {
|
||||
let s = s.trim();
|
||||
if !s.len().is_multiple_of(2) {
|
||||
bail!("hex string has odd length");
|
||||
}
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| anyhow::anyhow!("{}", e)))
|
||||
.collect()
|
||||
Ok(::hex::decode(s.trim())?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,6 +243,11 @@ pub async fn run(pool: &PgPool, params: AddParams<'_>, master_key: &[u8; 32]) ->
|
||||
tracing::warn!(error = %e, "failed to snapshot entry history before upsert");
|
||||
}
|
||||
|
||||
// Upsert the entry row. On conflict (existing entry with same user_id+folder+name),
|
||||
// the entry columns are replaced wholesale. The old secret associations are torn down
|
||||
// below within the same transaction, so the whole operation is atomic: if any step
|
||||
// after this point fails, the transaction rolls back and the entry reverts to its
|
||||
// pre-upsert state (including the version bump that happened in the DO UPDATE clause).
|
||||
let entry_id: Uuid = if let Some(uid) = params.user_id {
|
||||
sqlx::query_scalar(
|
||||
r#"INSERT INTO entries (user_id, folder, type, name, notes, tags, metadata, version, updated_at)
|
||||
|
||||
@@ -11,6 +11,7 @@ pub async fn list_for_user(
|
||||
offset: i64,
|
||||
) -> Result<Vec<AuditLogEntry>> {
|
||||
let limit = limit.clamp(1, 200);
|
||||
let offset = offset.max(0);
|
||||
|
||||
let rows = sqlx::query_as(
|
||||
"SELECT id, user_id, action, folder, type, name, detail, created_at \
|
||||
|
||||
@@ -31,6 +31,10 @@ pub struct DeleteParams<'a> {
|
||||
pub user_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// Maximum number of entries that can be deleted in a single bulk operation.
|
||||
/// Prevents accidental mass deletion when filters are too broad.
|
||||
pub const MAX_BULK_DELETE: usize = 1000;
|
||||
|
||||
/// Delete a single entry by id (multi-tenant: `user_id` must match).
|
||||
pub async fn delete_by_id(pool: &PgPool, entry_id: Uuid, user_id: Uuid) -> Result<DeleteResult> {
|
||||
let mut tx = pool.begin().await?;
|
||||
@@ -374,6 +378,16 @@ async fn delete_bulk(
|
||||
}
|
||||
let rows = q.fetch_all(&mut *tx).await?;
|
||||
|
||||
if rows.len() > MAX_BULK_DELETE {
|
||||
tx.rollback().await?;
|
||||
anyhow::bail!(
|
||||
"Bulk delete would affect {} entries (limit: {}). \
|
||||
Narrow your filters or delete entries individually.",
|
||||
rows.len(),
|
||||
MAX_BULK_DELETE,
|
||||
);
|
||||
}
|
||||
|
||||
let mut deleted = Vec::with_capacity(rows.len());
|
||||
for row in &rows {
|
||||
let entry_row: EntryRow = EntryRow {
|
||||
|
||||
@@ -402,8 +402,8 @@ pub async fn run(
|
||||
&mut tx,
|
||||
params.user_id,
|
||||
"update",
|
||||
"",
|
||||
"",
|
||||
&row.folder,
|
||||
&row.entry_type,
|
||||
params.name,
|
||||
serde_json::json!({
|
||||
"add_tags": params.add_tags,
|
||||
|
||||
Reference in New Issue
Block a user