chore(release): secrets-mcp 0.4.0
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 4m19s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 6s

Bump version for the N:N entry_secrets data model and related MCP/Web
changes. Remove superseded SQL migration artifacts; rely on auto-migrate.
Add structured errors, taxonomy normalization, and web i18n helpers.

Made-with: Cursor
This commit is contained in:
voson
2026-04-04 17:58:12 +08:00
parent b99d821644
commit 1518388374
29 changed files with 2285 additions and 1260 deletions

View File

@@ -5,11 +5,13 @@ use uuid::Uuid;
use crate::crypto;
use crate::db;
use crate::error::{AppError, DbErrorContext};
use crate::models::{EntryRow, EntryWriteRow};
use crate::service::add::{
collect_field_paths, collect_key_paths, flatten_json_fields, infer_secret_type, insert_path,
parse_key_path, parse_kv, remove_path,
collect_field_paths, collect_key_paths, flatten_json_fields, insert_path, parse_key_path,
parse_kv, remove_path,
};
use crate::taxonomy;
#[derive(Debug, serde::Serialize)]
pub struct UpdateResult {
@@ -35,6 +37,7 @@ pub struct UpdateParams<'a> {
pub meta_entries: &'a [String],
pub remove_meta: &'a [String],
pub secret_entries: &'a [String],
pub secret_types: &'a std::collections::HashMap<String, String>,
pub remove_secrets: &'a [String],
pub user_id: Option<Uuid>,
}
@@ -90,10 +93,7 @@ pub async fn run(
let row = match rows.len() {
0 => {
tx.rollback().await?;
anyhow::bail!(
"Not found: '{}'. Use `add` to create it first.",
params.name
)
return Err(AppError::NotFoundEntry.into());
}
1 => rows.into_iter().next().unwrap(),
_ => {
@@ -167,10 +167,7 @@ pub async fn run(
if result.rows_affected() == 0 {
tx.rollback().await?;
anyhow::bail!(
"Concurrent modification detected for '{}'. Please retry.",
params.name
);
return Err(AppError::ConcurrentModification.into());
}
for entry in params.secret_entries {
@@ -224,15 +221,21 @@ pub async fn run(
.execute(&mut *tx)
.await?;
} else {
let secret_type = params
.secret_types
.get(field_name)
.map(|s| s.as_str())
.unwrap_or("text");
let secret_id: Uuid = sqlx::query_scalar(
"INSERT INTO secrets (user_id, name, type, encrypted) VALUES ($1, $2, $3, $4) RETURNING id",
)
.bind(params.user_id)
.bind(field_name)
.bind(infer_secret_type(field_name))
.bind(field_name.to_string())
.bind(secret_type)
.bind(&encrypted)
.fetch_one(&mut *tx)
.await?;
.await
.map_err(|e| AppError::from_db_error(e, DbErrorContext::secret_name(field_name)))?;
sqlx::query("INSERT INTO entry_secrets (entry_id, secret_id) VALUES ($1, $2)")
.bind(row.id)
.bind(secret_id)
@@ -347,13 +350,13 @@ pub async fn update_fields_by_id(
user_id: Uuid,
params: UpdateEntryFieldsByIdParams<'_>,
) -> Result<()> {
if params.folder.len() > 128 {
if params.folder.chars().count() > 128 {
anyhow::bail!("folder must be at most 128 characters");
}
if params.entry_type.len() > 64 {
if params.entry_type.chars().count() > 64 {
anyhow::bail!("type must be at most 64 characters");
}
if params.name.len() > 256 {
if params.name.chars().count() > 256 {
anyhow::bail!("name must be at most 256 characters");
}
@@ -372,7 +375,7 @@ pub async fn update_fields_by_id(
Some(r) => r,
None => {
tx.rollback().await?;
anyhow::bail!("Entry not found");
return Err(AppError::NotFoundEntry.into());
}
};
@@ -395,17 +398,25 @@ pub async fn update_fields_by_id(
tracing::warn!(error = %e, "failed to snapshot entry history before web update");
}
let mut metadata_map = match params.metadata {
Value::Object(m) => m.clone(),
_ => Map::new(),
};
let normalized_type =
taxonomy::normalize_entry_type_and_metadata(params.entry_type, &mut metadata_map);
let normalized_metadata = Value::Object(metadata_map);
let res = sqlx::query(
"UPDATE entries SET folder = $1, type = $2, name = $3, notes = $4, tags = $5, metadata = $6, \
version = version + 1, updated_at = NOW() \
WHERE id = $7 AND version = $8",
)
.bind(params.folder)
.bind(params.entry_type)
.bind(&normalized_type)
.bind(params.name)
.bind(params.notes)
.bind(params.tags)
.bind(params.metadata)
.bind(&normalized_metadata)
.bind(row.id)
.bind(row.version)
.execute(&mut *tx)
@@ -414,16 +425,17 @@ pub async fn update_fields_by_id(
if let sqlx::Error::Database(ref d) = e
&& d.code().as_deref() == Some("23505")
{
return anyhow::anyhow!(
"An entry with this folder and name already exists for your account."
);
return AppError::ConflictEntryName {
folder: params.folder.to_string(),
name: params.name.to_string(),
};
}
e.into()
AppError::Internal(e.into())
})?;
if res.rows_affected() == 0 {
tx.rollback().await?;
anyhow::bail!("Concurrent modification detected. Please refresh and try again.");
return Err(AppError::ConcurrentModification.into());
}
crate::audit::log_tx(
@@ -431,7 +443,7 @@ pub async fn update_fields_by_id(
Some(user_id),
"update",
params.folder,
params.entry_type,
&normalized_type,
params.name,
serde_json::json!({
"source": "web",