feat(nn): entry–secret N:N, unique secret names, web unlink
Bump secrets-mcp to 0.3.8 (tag 0.3.7 already used). - Junction table entry_secrets; secrets user-scoped with type - Per-user unique secrets.name; link_secret_names on add - Manual migrations + migrate script; MCP/tool and Web updates Made-with: Cursor
This commit is contained in:
@@ -7,8 +7,8 @@ use crate::crypto;
|
||||
use crate::db;
|
||||
use crate::models::{EntryRow, EntryWriteRow};
|
||||
use crate::service::add::{
|
||||
collect_field_paths, collect_key_paths, flatten_json_fields, insert_path, parse_key_path,
|
||||
parse_kv, remove_path,
|
||||
collect_field_paths, collect_key_paths, flatten_json_fields, infer_secret_type, insert_path,
|
||||
parse_key_path, parse_kv, remove_path,
|
||||
};
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@@ -173,8 +173,6 @@ pub async fn run(
|
||||
);
|
||||
}
|
||||
|
||||
let new_version = row.version + 1;
|
||||
|
||||
for entry in params.secret_entries {
|
||||
let (path, field_value) = parse_kv(entry)?;
|
||||
let flat = flatten_json_fields("", &{
|
||||
@@ -192,7 +190,10 @@ pub async fn run(
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
let ef: Option<ExistingField> = sqlx::query_as(
|
||||
"SELECT id, encrypted FROM secrets WHERE entry_id = $1 AND field_name = $2",
|
||||
"SELECT s.id, s.encrypted \
|
||||
FROM entry_secrets es \
|
||||
JOIN secrets s ON s.id = es.secret_id \
|
||||
WHERE es.entry_id = $1 AND s.name = $2",
|
||||
)
|
||||
.bind(row.id)
|
||||
.bind(field_name)
|
||||
@@ -203,10 +204,8 @@ pub async fn run(
|
||||
&& let Err(e) = db::snapshot_secret_history(
|
||||
&mut tx,
|
||||
db::SecretSnapshotParams {
|
||||
entry_id: row.id,
|
||||
secret_id: ef.id,
|
||||
entry_version: row.version,
|
||||
field_name,
|
||||
name: field_name,
|
||||
encrypted: &ef.encrypted,
|
||||
action: "update",
|
||||
},
|
||||
@@ -216,16 +215,30 @@ pub async fn run(
|
||||
tracing::warn!(error = %e, "failed to snapshot secret field history");
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO secrets (entry_id, field_name, encrypted) VALUES ($1, $2, $3) \
|
||||
ON CONFLICT (entry_id, field_name) DO UPDATE SET \
|
||||
encrypted = EXCLUDED.encrypted, version = secrets.version + 1, updated_at = NOW()",
|
||||
)
|
||||
.bind(row.id)
|
||||
.bind(field_name)
|
||||
.bind(&encrypted)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if let Some(ef) = ef {
|
||||
sqlx::query(
|
||||
"UPDATE secrets SET encrypted = $1, version = version + 1, updated_at = NOW() WHERE id = $2",
|
||||
)
|
||||
.bind(&encrypted)
|
||||
.bind(ef.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
} else {
|
||||
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(&encrypted)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("INSERT INTO entry_secrets (entry_id, secret_id) VALUES ($1, $2)")
|
||||
.bind(row.id)
|
||||
.bind(secret_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +252,10 @@ pub async fn run(
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
let field: Option<FieldToDelete> = sqlx::query_as(
|
||||
"SELECT id, encrypted FROM secrets WHERE entry_id = $1 AND field_name = $2",
|
||||
"SELECT s.id, s.encrypted \
|
||||
FROM entry_secrets es \
|
||||
JOIN secrets s ON s.id = es.secret_id \
|
||||
WHERE es.entry_id = $1 AND s.name = $2",
|
||||
)
|
||||
.bind(row.id)
|
||||
.bind(&field_name)
|
||||
@@ -250,10 +266,8 @@ pub async fn run(
|
||||
if let Err(e) = db::snapshot_secret_history(
|
||||
&mut tx,
|
||||
db::SecretSnapshotParams {
|
||||
entry_id: row.id,
|
||||
secret_id: f.id,
|
||||
entry_version: new_version,
|
||||
field_name: &field_name,
|
||||
name: &field_name,
|
||||
encrypted: &f.encrypted,
|
||||
action: "delete",
|
||||
},
|
||||
@@ -262,10 +276,19 @@ pub async fn run(
|
||||
{
|
||||
tracing::warn!(error = %e, "failed to snapshot secret field history before delete");
|
||||
}
|
||||
sqlx::query("DELETE FROM secrets WHERE id = $1")
|
||||
sqlx::query("DELETE FROM entry_secrets WHERE entry_id = $1 AND secret_id = $2")
|
||||
.bind(row.id)
|
||||
.bind(f.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"DELETE FROM secrets s \
|
||||
WHERE s.id = $1 \
|
||||
AND NOT EXISTS (SELECT 1 FROM entry_secrets es WHERE es.secret_id = s.id)",
|
||||
)
|
||||
.bind(f.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user