diff --git a/desktop/src-tauri/src/app_state.rs b/desktop/src-tauri/src/app_state.rs index 2d96b00cd5..349e93f975 100644 --- a/desktop/src-tauri/src/app_state.rs +++ b/desktop/src-tauri/src/app_state.rs @@ -888,10 +888,10 @@ fn persist_imported_identity_impl( } } -/// Public entry point binding [`persist_imported_identity_impl`] to the shared -/// [`crate::secret_store::SecretStore`]. See the impl for the persistence policy. +/// Public entry point for imported-identity persistence over any +/// [`IdentityKeyStore`] (production passes the shared `SecretStore`). pub(crate) fn persist_imported_identity( - store: &crate::secret_store::SecretStore, + store: &impl IdentityKeyStore, keys: &Keys, legacy_path: &std::path::Path, data_dir: &std::path::Path, diff --git a/desktop/src-tauri/src/commands/identity.rs b/desktop/src-tauri/src/commands/identity.rs index 7473b84796..2ad5b70759 100644 --- a/desktop/src-tauri/src/commands/identity.rs +++ b/desktop/src-tauri/src/commands/identity.rs @@ -383,9 +383,24 @@ pub async fn import_identity( password: Option, app_handle: tauri::AppHandle, ) -> Result { + let data_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| format!("app data dir: {e}"))?; + let store = crate::secret_store::SecretStore::shared(crate::app_state::keyring_service()); // Normal import: no supersession fence, both validity gates always pass — // there is no queued-task currency to check. - run_identity_transition(app_handle, nsec, password, None, || Ok(()), || Ok(())).await + run_identity_transition( + app_handle, + nsec, + password, + None, + store, + data_dir, + || Ok(()), + || Ok(()), + ) + .await } /// Finish an imported identity commit whose durable persist already succeeded diff --git a/desktop/src-tauri/src/commands/identity_transition.rs b/desktop/src-tauri/src/commands/identity_transition.rs index fedd0ae593..cbacc07407 100644 --- a/desktop/src-tauri/src/commands/identity_transition.rs +++ b/desktop/src-tauri/src/commands/identity_transition.rs @@ -102,14 +102,21 @@ fn compensate_import_drain( /// runtimes compensate, but revoked owner-identity durable capabilities stay /// revoked. This is design-conformant fail-closed churn, not a split state — /// see `CROSS_WORKSPACE_AGENT_LIBRARY.md` §3.3a (barrier sequence is fixed). -pub(crate) async fn run_identity_transition( +#[allow(clippy::too_many_arguments)] +pub(crate) async fn run_identity_transition( app_handle: tauri::AppHandle, nsec: String, password: Option, commit_fence: Option>>, + store: &'static S, + data_dir: std::path::PathBuf, early_validity_check: impl FnOnce() -> Result<(), String>, late_validity_check: impl FnOnce() -> Result<(), String> + Send + 'static, -) -> Result { +) -> Result +where + R: tauri::Runtime, + S: crate::app_state::IdentityKeyStore + Send + Sync + 'static, +{ // ── Layer 1: identity_mutation (async serialization lock) ──────────────── // Held for the full import to prevent a concurrent stale persist from // overwriting the imported key. Lock order: identity_mutation → @@ -149,6 +156,8 @@ pub(crate) async fn run_identity_transition( nsec, password, commit_fence, + store, + data_dir, late_validity_check, ) }) @@ -200,13 +209,19 @@ pub(crate) fn commit_under_fence( /// either compensates (cancelled during drain, before the fence is taken) or /// loses to the committed recovery (cancelled after the fenced commit begins). /// Normal import supplies `None` + an always-`Ok(())` check. -fn import_identity_blocking( +fn import_identity_blocking( app_handle: tauri::AppHandle, nsec: String, password: Option, commit_fence: Option>>, + store: &S, + data_dir: std::path::PathBuf, validity_check: impl FnOnce() -> Result<(), String>, -) -> Result { +) -> Result +where + R: tauri::Runtime, + S: crate::app_state::IdentityKeyStore, +{ // NIP-49 backups require a passphrase and decrypt entirely in Rust. // Raw nsec/hex input follows the existing parser path unchanged. let password = password.map(zeroize::Zeroizing::new); @@ -217,10 +232,6 @@ fn import_identity_blocking( let state = app_handle.state::(); - let data_dir = app_handle - .path() - .app_data_dir() - .map_err(|e| format!("app data dir: {e}"))?; std::fs::create_dir_all(&data_dir).map_err(|e| format!("create app data dir: {e}"))?; let key_path = data_dir.join("identity.key"); @@ -316,8 +327,9 @@ fn import_identity_blocking( // check and the durable dispatch nor interleave with it. The P28-C1 journal // is written ONLY after the validity gate passes, so a superseded recovery // leaves no journal residue. The persist is CLASSIFIED against durable fact - // (P27-C1) — never the kernel's `Ok`/`Err`. - let store = crate::secret_store::SecretStore::shared(crate::app_state::keyring_service()); + // (P27-C1) — never the kernel's `Ok`/`Err`. `store` and `data_dir` are the + // durable-persist seams injected by the caller (production: the shared + // `SecretStore` + real `app_data_dir()`; tests: a fake store + tempdir). let pending = crate::identity_transition_journal::IdentityTransitionPending { from_pubkey, to_pubkey, @@ -628,6 +640,8 @@ mod tests { "nsec-invalid-must-never-be-parsed".to_string(), None, None, + crate::secret_store::SecretStore::shared(crate::app_state::keyring_service()), + std::env::temp_dir(), || Err("superseded while queued".to_string()), || unreachable!("late gate must not run after an early-gate rejection"), ) diff --git a/desktop/src-tauri/src/commands/pairing.rs b/desktop/src-tauri/src/commands/pairing.rs index c4d20d1f19..af73d7f428 100644 --- a/desktop/src-tauri/src/commands/pairing.rs +++ b/desktop/src-tauri/src/commands/pairing.rs @@ -479,11 +479,17 @@ async fn import_recovered_identity( // identity). `ensure_pairing_task_is_current` is an idempotent currency // read, so both gates get an independent clone of the same check. let early_generation = Arc::clone(&generation); + let data_dir = tauri::Manager::path(app) + .app_data_dir() + .map_err(|e| format!("app data dir: {e}"))?; + let store = crate::secret_store::SecretStore::shared(crate::app_state::keyring_service()); crate::commands::identity::run_identity_transition( app.clone(), nsec.trim().to_string(), None, Some(Arc::clone(generation_fence)), + store, + data_dir, move || ensure_pairing_task_is_current(&early_generation, task_generation), move || ensure_pairing_task_is_current(&generation, task_generation), )