feat(v3): migrate workspace to API, Tauri desktop, and v3 crates; remove legacy MCP stack
Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
- Add apps/api, desktop Tauri shell, domain/application/crypto/device-auth/infrastructure-db - Replace desktop-daemon vault integration; drop secrets-core and secrets-mcp* - Ignore apps/desktop/dist and generated Tauri icons; document icon/dist steps in AGENTS.md - Apply rustfmt; fix clippy (collapsible_if, HTTP method as str)
This commit is contained in:
16
crates/domain/Cargo.toml
Normal file
16
crates/domain/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "secrets-domain"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "secrets_domain"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5.3"
|
||||
chrono.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
68
crates/domain/src/auth.rs
Normal file
68
crates/domain/src/auth.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub email: Option<String>,
|
||||
pub name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub key_salt: Option<Vec<u8>>,
|
||||
pub key_check: Option<Vec<u8>>,
|
||||
pub key_params: Option<Value>,
|
||||
pub key_version: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Device {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: String,
|
||||
pub platform: String,
|
||||
pub client_version: String,
|
||||
pub device_fingerprint: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub last_seen_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceLoginToken {
|
||||
pub id: Uuid,
|
||||
pub device_id: Uuid,
|
||||
pub token_hash: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub last_seen_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LoginMethod {
|
||||
GoogleOauth,
|
||||
DeviceToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LoginResult {
|
||||
Success,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientLoginEvent {
|
||||
pub id: i64,
|
||||
pub user_id: Uuid,
|
||||
pub device_id: Uuid,
|
||||
pub device_name: String,
|
||||
pub platform: String,
|
||||
pub client_version: String,
|
||||
pub ip_addr: Option<String>,
|
||||
pub forwarded_ip: Option<String>,
|
||||
pub login_method: LoginMethod,
|
||||
pub login_result: LoginResult,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
138
crates/domain/src/cipher.rs
Normal file
138
crates/domain/src/cipher.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CipherType {
|
||||
Login,
|
||||
ApiKey,
|
||||
SecureNote,
|
||||
SshKey,
|
||||
Identity,
|
||||
Card,
|
||||
}
|
||||
|
||||
impl CipherType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Login => "login",
|
||||
Self::ApiKey => "api_key",
|
||||
Self::SecureNote => "secure_note",
|
||||
Self::SshKey => "ssh_key",
|
||||
Self::Identity => "identity",
|
||||
Self::Card => "card",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Self {
|
||||
match input {
|
||||
"login" => Self::Login,
|
||||
"api_key" => Self::ApiKey,
|
||||
"secure_note" => Self::SecureNote,
|
||||
"ssh_key" => Self::SshKey,
|
||||
"identity" => Self::Identity,
|
||||
"card" => Self::Card,
|
||||
_ => Self::SecureNote,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct CustomField {
|
||||
pub name: String,
|
||||
pub value: Value,
|
||||
#[serde(default)]
|
||||
pub sensitive: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct LoginPayload {
|
||||
#[serde(default)]
|
||||
pub username: Option<String>,
|
||||
#[serde(default)]
|
||||
pub uris: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub password: Option<String>,
|
||||
#[serde(default)]
|
||||
pub totp: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct ApiKeyPayload {
|
||||
#[serde(default)]
|
||||
pub client_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub secret: Option<String>,
|
||||
#[serde(default)]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub host: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct SecureNotePayload {
|
||||
#[serde(default)]
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct SshKeyPayload {
|
||||
#[serde(default)]
|
||||
pub username: Option<String>,
|
||||
#[serde(default)]
|
||||
pub host: Option<String>,
|
||||
#[serde(default)]
|
||||
pub port: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub private_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub passphrase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ItemPayload {
|
||||
Login(LoginPayload),
|
||||
ApiKey(ApiKeyPayload),
|
||||
SecureNote(SecureNotePayload),
|
||||
SshKey(SshKeyPayload),
|
||||
}
|
||||
|
||||
impl Default for ItemPayload {
|
||||
fn default() -> Self {
|
||||
Self::SecureNote(SecureNotePayload::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct CipherView {
|
||||
pub id: Uuid,
|
||||
pub cipher_type: CipherType,
|
||||
pub name: String,
|
||||
pub folder: String,
|
||||
#[serde(default)]
|
||||
pub notes: Option<String>,
|
||||
#[serde(default)]
|
||||
pub custom_fields: Vec<CustomField>,
|
||||
#[serde(default)]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub revision_date: DateTime<Utc>,
|
||||
pub payload: ItemPayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Cipher {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub object_kind: String,
|
||||
pub cipher_type: CipherType,
|
||||
pub revision: i64,
|
||||
pub cipher_version: i32,
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub content_hash: String,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
15
crates/domain/src/error.rs
Normal file
15
crates/domain/src/error.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DomainError {
|
||||
#[error("resource not found")]
|
||||
NotFound,
|
||||
#[error("resource already exists")]
|
||||
Conflict,
|
||||
#[error("validation failed: {0}")]
|
||||
Validation(String),
|
||||
#[error("authentication failed")]
|
||||
AuthenticationFailed,
|
||||
#[error("decryption failed")]
|
||||
DecryptionFailed,
|
||||
}
|
||||
37
crates/domain/src/kdf.rs
Normal file
37
crates/domain/src/kdf.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::DomainError;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum KdfType {
|
||||
Argon2id,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct KdfConfig {
|
||||
pub kdf_type: KdfType,
|
||||
pub memory_kib: u32,
|
||||
pub iterations: u32,
|
||||
pub parallelism: u32,
|
||||
}
|
||||
|
||||
impl Default for KdfConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kdf_type: KdfType::Argon2id,
|
||||
memory_kib: 64 * 1024,
|
||||
iterations: 3,
|
||||
parallelism: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KdfConfig {
|
||||
pub fn build_argon2(&self) -> Result<Argon2<'static>, DomainError> {
|
||||
let params = Params::new(self.memory_kib, self.iterations, self.parallelism, Some(32))
|
||||
.map_err(|err| DomainError::Validation(err.to_string()))?;
|
||||
Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
|
||||
}
|
||||
}
|
||||
19
crates/domain/src/lib.rs
Normal file
19
crates/domain/src/lib.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
pub mod auth;
|
||||
pub mod cipher;
|
||||
pub mod error;
|
||||
pub mod kdf;
|
||||
pub mod sync;
|
||||
pub mod vault_object;
|
||||
|
||||
pub use auth::{ClientLoginEvent, Device, DeviceLoginToken, LoginMethod, LoginResult, User};
|
||||
pub use cipher::{
|
||||
ApiKeyPayload, Cipher, CipherType, CipherView, CustomField, ItemPayload, LoginPayload,
|
||||
SecureNotePayload, SshKeyPayload,
|
||||
};
|
||||
pub use error::DomainError;
|
||||
pub use kdf::{KdfConfig, KdfType};
|
||||
pub use sync::{
|
||||
SyncAcceptedChange, SyncConflict, SyncPullRequest, SyncPullResponse, SyncPushRequest,
|
||||
SyncPushResponse,
|
||||
};
|
||||
pub use vault_object::{VaultObjectChange, VaultObjectEnvelope, VaultObjectKind, VaultTombstone};
|
||||
47
crates/domain/src/sync.rs
Normal file
47
crates/domain/src/sync.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::vault_object::{VaultObjectChange, VaultObjectEnvelope, VaultTombstone};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SyncPullRequest {
|
||||
pub cursor: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub include_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SyncPullResponse {
|
||||
pub server_revision: i64,
|
||||
pub next_cursor: i64,
|
||||
pub has_more: bool,
|
||||
pub objects: Vec<VaultObjectEnvelope>,
|
||||
pub tombstones: Vec<VaultTombstone>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SyncPushRequest {
|
||||
pub changes: Vec<VaultObjectChange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SyncAcceptedChange {
|
||||
pub change_id: uuid::Uuid,
|
||||
pub object_id: uuid::Uuid,
|
||||
pub revision: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SyncConflict {
|
||||
pub change_id: uuid::Uuid,
|
||||
pub object_id: uuid::Uuid,
|
||||
pub reason: String,
|
||||
pub server_object: Option<VaultObjectEnvelope>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SyncPushResponse {
|
||||
pub server_revision: i64,
|
||||
pub accepted: Vec<SyncAcceptedChange>,
|
||||
pub conflicts: Vec<SyncConflict>,
|
||||
}
|
||||
48
crates/domain/src/vault_object.rs
Normal file
48
crates/domain/src/vault_object.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VaultObjectKind {
|
||||
Cipher,
|
||||
}
|
||||
|
||||
impl VaultObjectKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Cipher => "cipher",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct VaultObjectEnvelope {
|
||||
pub object_id: Uuid,
|
||||
pub object_kind: VaultObjectKind,
|
||||
pub revision: i64,
|
||||
pub cipher_version: i32,
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub content_hash: String,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct VaultObjectChange {
|
||||
pub change_id: Uuid,
|
||||
pub object_id: Uuid,
|
||||
pub object_kind: VaultObjectKind,
|
||||
pub operation: String,
|
||||
pub base_revision: Option<i64>,
|
||||
pub cipher_version: Option<i32>,
|
||||
pub ciphertext: Option<Vec<u8>>,
|
||||
pub content_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct VaultTombstone {
|
||||
pub object_id: Uuid,
|
||||
pub revision: i64,
|
||||
pub deleted_at: DateTime<Utc>,
|
||||
}
|
||||
Reference in New Issue
Block a user