{"repo": "christfollower873461/codexvault", "files": {"src-tauri/src/exports.rs": {"cf477b05bb8d21604bd21933df1a9422d71b8640": {"t": 1786068337.3350387, "n": 20502, "h": "41d55ae77e1b3074", "c": "use std::{cmp::Ordering, collections::BTreeMap};\n\nuse serde::Serialize;\n\nuse crate::{\n error::{AppError, AppResult},\n models::{now_timestamp, EntryStatus, VaultEntry, VaultPayload},\n};\n\npub fn render_export(payload: &VaultPayload, ids: &[String], format: &str) -> AppResult {\n let export_format = ExportFormat::parse(format)?;\n let entries = select_entries(payload, ids)?;\n\n match export_format {\n ExportFormat::Env => render_env(&entries),\n ExportFormat::GenericJson => render_generic_json(&entries),\n ExportFormat::OpenClaw => render_openclaw(&entries),\n ExportFormat::OpenClawBundle => render_openclaw_bundle(&entries),\n ExportFormat::ProviderSnippet => render_provider_snippets(&entries),\n }\n}\n\nfn select_entries<'a>(payload: &'a VaultPayload, ids: &[String]) -> AppResult> {\n if ids.is_empty() {\n return Ok(payload.entries.iter().collect());\n }\n\n let mut selected = Vec::new();\n for id in ids {\n let entry = payload\n .entries\n .iter()\n .find(|entry| entry.id == *id)\n .ok_or(AppError::EntryNotFound)?;\n selected.push(entry);\n }\n Ok(selected)\n}\n\nfn render_env(entries: &[&VaultEntry]) -> AppResult {\n Ok(entries\n .iter()\n .map(|entry| {\n Ok(format!(\n \"{}={}\",\n entry.env_var_name,\n serde_json::to_string(&entry.secret_value)?\n ))\n })\n .collect::, serde_json::Error>>()?\n .join(\"\\n\"))\n}\n\nfn render_openclaw(entries: &[&VaultEntry]) -> AppResult {\n let selection = build_openclaw_selection(entries);\n let mut lines = vec![\n \"# CodexVault OpenClaw env export\".to_string(),\n format!(\"# generatedAt={}\", now_timestamp()),\n \"# selectionPolicy: one key per OpenClaw env var; prefer active, then old; revoked excluded.\"\n .to_string(),\n ];\n\n if selection.selected.is_empty() {\n lines.push(\"# no exportable entries matched the current selection\".to_string());\n return Ok(lines.join(\"\\n\"));\n }\n\n for selected in &selection.selected {\n if selected.used_old_fallback {\n lines.push(format!(\n \"# NOTE {} uses an old credential because no active entry matched this provider\",\n selected.env_name\n ));\n }\n lines.push(format!(\n \"{}={}\",\n selected.env_name,\n serde_json::to_string(&selected.entry.secret_value)?\n ));\n }\n\n if !selection.skipped.is_empty() {\n lines.push(String::new());\n lines.push(\"# skippedEntries:\".to_string());\n for skipped in &selection.skipped {\n lines.push(format!(\n \"# - {} :: {} [{}] ({})\",\n skipped.env_name,\n skipped.entry.name,\n skipped.entry.provider,\n skipped.reason.label()\n ));\n }\n }\n\n Ok(lines.join(\"\\n\"))\n}\n\nfn render_openclaw_bundle(entries: &[&VaultEntry]) -> AppResult {\n #[derive(Serialize)]\n #[serde(rename_all = \"camelCase\")]\n struct OpenClawSelectionPolicy {\n status_priority: [&'static str; 3],\n dedupe: &'static str,\n revoked_behavior: &'static str,\n }\n\n #[derive(Serialize)]\n #[serde(rename_all = \"camelCase\")]\n struct OpenClawProviderBinding<'a> {\n provider: String,\n env_var_name: String,\n entry_id: &'a str,\n entry_name: &'a str,\n status: &'a EntryStatus,\n environment: &'a str,\n model_family: &'a Option,\n models: &'a Vec,\n tags: &'a Vec,\n agent_access_tags: &'a Vec,\n used_old_fallback: bool,\n }\n\n #[derive(Serialize)]\n #[serde(rename_all = \"camelCase\")]\n struct OpenClawSkippedEntry<'a> {\n env_var_name: String,\n reason: &'static str,\n entry_id: &'a str,\n entry_name: &'a str,\n provider: &'a str,\n status: &'a EntryStatus,\n environment: &'a str,\n }\n\n #[derive(Serialize)]\n #[serde(rename_all = \"camelCase\")]\n struct OpenClawBundle<'a> {\n format: &'static str,\n version: u8,\n generated_at: String,\n selection_policy: OpenClawSelectionPolicy,\n env: BTreeMap,\n providers: Vec>,\n skipped: Vec>,\n }\n\n let selection = build_openclaw_selection(entries);\n\n let mut env = BTreeMap::new();\n let mut providers = Vec::new();\n for selected in &selection.selected {\n env.insert(\n selected.env_name.clone(),\n selected.entry.secret_value.clone(),\n );\n providers.push(OpenClawProviderBinding {\n provider: openclaw_provider_key(selected.entry),\n env_var_name: selected.env_name.clone(),\n entry_id: &selected.entry.id,\n entry_name: &selected.entry.name,\n status: &selected.entry.status,\n environment: &selected.entry.environment,\n model_family: &selected.entry.model_family,\n models: &selected.entry.models,\n tags: &selected.entry.tags,\n agent_access_tags: &selected.entry.agent_access_tags,\n used_old_fallback: selected.used_old_fallback,\n });\n }\n\n providers.sort_by(|left, right| left.provider.cmp(&right.provider));\n\n let skipped = selection\n .skipped\n .iter()\n .map(|entry| OpenClawSkippedEntry {\n env_var_name: entry.env_name.clone(),\n reason: entry.reason.label(),\n entry_id: &entry.entry.id,\n entry_name: &entry.entry.name,\n provider: &entry.entry.provider,\n status: &entry.entry.status,\n environment: &entry.entry.environment,\n })\n .collect::>();\n\n let bundle = OpenClawBundle {\n format: \"codexvault.openclaw.bundle\",\n version: 1,\n generated_at: now_timestamp(),\n selection_policy: OpenClawSelectionPolicy {\n status_priority: [\"active\", \"old\", \"revoked\"],\n dedupe: \"one entry per OpenClaw env var name\",\n revoked_behavior: \"excluded from env output\",\n },\n env,\n providers,\n skipped,\n };\n\n serde_json::to_string_pretty(&bundle).map_err(Into::into)\n}\n\nfn render_generic_json(entries: &[&VaultEntry]) -> AppResult {\n #[derive(Serialize)]\n #[serde(rename_all = \"camelCase\")]\n struct GenericJsonEntry<'a> {\n id: &'a str,\n name: &'a str,\n provider: &'a str,\n env_var_name: &'a str,\n secret_value: &'a str,\n model_family: &'a Option,\n models: &'a Vec,\n tags: &'a Vec,\n notes: &'a str,\n environment: &'a str,\n agent_access_tags: &'a Vec,\n status: &'a EntryStatus,\n created_at: &'a str,\n updated_at: &'a str,\n rotated_at: &'a Option,\n last_tested_at: &'a Option,\n last_used_at: &'a Option,\n }\n\n let payload = entries\n .iter()\n .map(|entry| GenericJsonEntry {\n id: &entry.id,\n name: &entry.name,\n provider: &entry.provider,\n env_var_name: &entry.env_var_name,\n secret_value: &entry.secret_value,\n model_family: &entry.model_family,\n models: &entry.models,\n tags: &entry.tags,\n notes: &entry.notes,\n environment: &entry.environment,\n agent_access_tags: &entry.agent_access_tags,\n status: &entry.status,\n created_at: &entry.created_at,\n updated_at: &entry.updated_at,\n rotated_at: &entry.rotated_at,\n last_tested_at: &entry.last_tested_at,\n last_used_at: &entry.last_used_at,\n })\n .collect::>();\n\n serde_json::to_string_pretty(&payload).map_err(Into::into)\n}\n\nfn render_provider_snippets(entries: &[&VaultEntry]) -> AppResult {\n #[derive(Serialize)]\n #[serde(rename_all = \"camelCase\")]\n struct ProviderSnippet<'a> {\n provider: &'a str,\n env_var_name: &'a str,\n api_key: &'a str,\n environment: &'a str,\n model_family: &'a Option,\n models: &'a Vec,\n status: &'a EntryStatus,\n }\n\n let payload = entries\n .iter()\n .map(|entry| ProviderSnippet {\n provider: &entry.provider,\n env_var_name: &entry.env_var_name,\n api_key: &entry.secret_value,\n environment: &entry.environment,\n model_family: &entry.model_family,\n models: &entry.models,\n status: &entry.status,\n })\n .collect::>();\n\n serde_json::to_string_pretty(&payload).map_err(Into::into)\n}\n\nfn openclaw_env_name(entry: &VaultEntry) -> String {\n match entry.provider.trim().to_ascii_lowercase().as_str() {\n \"openai\" => \"OPENAI_API_KEY\".into(),\n \"anthropic\" => \"ANTHROPIC_API_KEY\".into(),\n \"azure\" | \"azure openai\" | \"azure-openai\" => \"AZURE_OPENAI_API_KEY\".into(),\n \"openrouter\" => \"OPENROUTER_API_KEY\".into(),\n \"groq\" => \"GROQ_API_KEY\".into(),\n \"google\" | \"gemini\" => \"GEMINI_API_KEY\".into(),\n \"xai\" => \"XAI_API_KEY\".into(),\n \"mistral\" => \"MISTRAL_API_KEY\".into(),\n \"cohere\" => \"COHERE_API_KEY\".into(),\n \"deepseek\" => \"DEEPSEEK_API_KEY\".into(),\n \"together\" => \"TOGETHER_API_KEY\".into(),\n _ => entry.env_var_name.clone(),\n }\n}\n\nfn openclaw_provider_key(entry: &VaultEntry) -> String {\n match entry.provider.trim().to_ascii_lowercase().as_str() {\n \"openai\" => \"openai\".into(),\n \"anthropic\" => \"anthropic\".into(),\n \"azure\" | \"azure openai\" | \"azure-openai\" => \"azure-openai\".into(),\n \"openrouter\" => \"openrouter\".into(),\n \"groq\" => \"groq\".into(),\n \"google\" | \"gemini\" => \"gemini\".into(),\n \"xai\" => \"xai\".into(),\n \"mistral\" => \"mistral\".into(),\n \"cohere\" => \"cohere\".into(),\n \"deepseek\" => \"deepseek\".into(),\n \"together\" => \"together\".into(),\n _ => entry.provider.trim().to_ascii_lowercase(),\n }\n}\n\n#[derive(Clone, Copy)]\nenum OpenClawSkipReason {\n Revoked,\n ShadowedByHigherPriorityEntry,\n}\n\nimpl OpenClawSkipReason {\n fn label(self) -> &'static str {\n match self {\n Self::Revoked => \"revoked\",\n Self::ShadowedByHigherPriorityEntry => \"shadowedByHigherPriorityEntry\",\n }\n }\n}\n\nstruct OpenClawSelected<'a> {\n env_name: String,\n entry: &'a VaultEntry,\n used_old_fallback: bool,\n}\n\nstruct OpenClawSkipped<'a> {\n env_name: String,\n entry: &'a VaultEntry,\n reason: OpenClawSkipReason,\n}\n\nstruct OpenClawSelection<'a> {\n selected: Vec>,\n skipped: Vec>,\n}\n\nfn build_openclaw_selection<'a>(entries: &[&'a VaultEntry]) -> OpenClawSelection<'a> {\n let mut by_env_name: BTreeMap> = BTreeMap::new();\n for entry in entries {\n by_env_name\n .entry(openclaw_env_name(entry))\n .or_default()\n .push(*entry);\n }\n\n let mut selected = Vec::new();\n let mut skipped = Vec::new();\n\n for (env_name, mut candidates) in by_env_name {\n candidates.sort_by(compare_openclaw_candidates);\n let has_active_candidate = candidates\n .iter()\n .any(|entry| matches!(entry.status, EntryStatus::Active));\n let selected_index = candidates\n .iter()\n .position(|entry| !matches!(entry.status, EntryStatus::Revoked));\n\n if let Some(index) = selected_index {\n let selected_entry = candidates[index];\n selected.push(OpenClawSelected {\n env_name: env_name.clone(),\n entry: selected_entry,\n used_old_fallback: !has_active_candidate\n && matches!(selected_entry.status, EntryStatus::Old),\n });\n\n for (candidate_index, candidate) in candidates.into_iter().enumerate() {\n if candidate_index == index {\n continue;\n }\n skipped.push(OpenClawSkipped {\n env_name: env_name.clone(),\n entry: candidate,\n reason: if matches!(candidate.status, EntryStatus::Revoked) {\n OpenClawSkipReason::Revoked\n } else {\n OpenClawSkipReason::ShadowedByHigherPriorityEntry\n },\n });\n }\n } else {\n for candidate in candidates {\n skipped.push(OpenClawSkipped {\n env_name: env_name.clone(),\n entry: candidate,\n reason: OpenClawSkipReason::Revoked,\n });\n }\n }\n }\n\n skipped.sort_by(|left, right| {\n left.env_name\n .cmp(&right.env_name)\n .then_with(|| left.entry.name.cmp(&right.entry.name))\n .then_with(|| left.entry.id.cmp(&right.entry.id))\n });\n\n OpenClawSelection { selected, skipped }\n}\n\nfn compare_openclaw_candidates(left: &&VaultEntry, right: &&VaultEntry) -> Ordering {\n openclaw_status_priority(&right.status)\n .cmp(&openclaw_status_priority(&left.status))\n .then_with(|| openclaw_freshness(right).cmp(openclaw_freshness(left)))\n .then_with(|| right.updated_at.cmp(&left.updated_at))\n .then_with(|| left.name.cmp(&right.name))\n .then_with(|| left.id.cmp(&right.id))\n}\n\nfn openclaw_status_priority(status: &EntryStatus) -> u8 {\n match status {\n EntryStatus::Active => 3,\n EntryStatus::Old => 2,\n EntryStatus::Revoked => 1,\n }\n}\n\nfn openclaw_freshness(entry: &VaultEntry) -> &str {\n entry\n .last_tested_at\n .as_deref()\n .or(entry.rotated_at.as_deref())\n .unwrap_or(&entry.updated_at)\n}\n\nenum ExportFormat {\n Env,\n GenericJson,\n OpenClaw,\n OpenClawBundle,\n ProviderSnippet,\n}\n\nimpl ExportFormat {\n fn parse(format: &str) -> AppResult {\n match format {\n \"env\" => Ok(Self::Env),\n \"genericJson\" => Ok(Self::GenericJson),\n \"openClaw\" => Ok(Self::OpenClaw),\n \"openClawBundle\" => Ok(Self::OpenClawBundle),\n \"providerSnippet\" => Ok(Self::ProviderSnippet),\n _ => Err(AppError::Validation(\"Unsupported export format.\".into())),\n }\n }\n}\n\n#[cfg(test)]\nmod tests {\n use serde_json::Value;\n\n use crate::models::{EntryStatus, VaultEntry, VaultPayload, VaultSettings};\n\n use super::render_export;\n\n fn sample_entry(\n id: &str,\n name: &str,\n provider: &str,\n status: EntryStatus,\n secret: &str,\n updated_at: &str,\n ) -> VaultEntry {\n VaultEntry {\n id: id.into(),\n name: name.into(),\n provider: provider.into(),\n env_var_name: format!(\n \"{}_API_KEY\",\n provider.to_ascii_uppercase().replace(' ', \"_\")\n ),\n secret_value: secret.into(),\n model_family: Some(\"gpt-4.1\".into()),\n models: vec![\"gpt-4.1\".into()],\n tags: vec![\"prod\".into()],\n notes: \"Primary\".into(),\n environment: \"production\".into(),\n agent_access_tags: vec![\"writer\".into()],\n status,\n created_at: \"2026-03-01T10:00:00Z\".into(),\n updated_at: updated_at.into(),\n rotated_at: None,\n last_tested_at: None,\n last_used_at: None,\n }\n }\n\n fn sample_payload(entries: Vec) -> VaultPayload {\n VaultPayload {\n vault_name: \"CodexVault\".into(),\n settings: VaultSettings::default(),\n entries,\n }\n }\n\n #[test]\n fn env_export_uses_env_var_names() {\n let payload = sample_payload(vec![sample_entry(\n \"entry-1\",\n \"OpenAI Primary\",\n \"OpenAI\",\n EntryStatus::Active,\n \"sk-secret\",\n \"2026-03-21T10:00:00Z\",\n )]);\n let export = render_export(&payload, &[], \"env\").unwrap();\n assert!(export.contains(\"OPENAI_API_KEY\"));\n }\n\n #[test]\n fn openclaw_export_uses_provider_defaults_and_skips_revoked() {\n let payload = sample_payload(vec![\n sample_entry(\n \"entry-openai-active\",\n \"OpenAI Active\",\n \"OpenAI\",\n EntryStatus::Active,\n \"sk-openai-active\",\n \"2026-03-20T10:00:00Z\",\n ),\n sample_entry(\n \"entry-openai-old\",\n \"OpenAI Old\",\n \"OpenAI\",\n EntryStatus::Old,\n \"sk-openai-old\",\n \"2026-03-21T10:00:00Z\",\n ),\n sample_entry(\n \"entry-openai-revoked\",\n \"OpenAI Revoked\",\n \"OpenAI\",\n EntryStatus::Revoked,\n \"sk-openai-revoked\",\n \"2026-03-21T12:00:00Z\",\n ),\n sample_entry(\n \"entry-anthropic-old\",\n \"Anthropic Old\",\n \"Anthropic\",\n EntryStatus::Old,\n \"sk-anthropic-old\",\n \"2026-03-19T10:00:00Z\",\n ),\n sample_entry(\n \"entry-groq-revoked\",\n \"Groq Revoked\",\n \"Groq\",\n EntryStatus::Revoked,\n \"sk-groq-revoked\",\n \"2026-03-19T10:00:00Z\",\n ),\n ]);\n\n let export = render_export(&payload, &[], \"openClaw\").unwrap();\n\n assert!(export.contains(\"OPENAI_API_KEY\"));\n assert!(export.contains(\"sk-openai-active\"));\n assert!(!export.contains(\"sk-openai-old\"));\n assert!(export.contains(\"ANTHROPIC_API_KEY\"));\n assert!(export.contains(\"sk-anthropic-old\"));\n assert!(!export.contains(\"GROQ_API_KEY=\"));\n assert!(export.contains(\"uses an old credential\"));\n assert!(export.contains(\"skippedEntries\"));\n assert!(export.contains(\"revoked\"));\n }\n\n #[test]\n fn openclaw_bundle_export_is_auditable_and_deterministic() {\n let payload = sample_payload(vec![\n sample_entry(\n \"entry-openai-active\",\n \"OpenAI Active\",\n \"OpenAI\",\n EntryStatus::Active,\n \"sk-openai-active\",\n \"2026-03-20T10:00:00Z\",\n ),\n sample_entry(\n \"entry-openai-old\",\n \"OpenAI Old\",\n \"OpenAI\",\n EntryStatus::Old,\n \"sk-openai-old\",\n \"2026-03-21T10:00:00Z\",\n ),\n sample_entry(\n \"entry-anthropic-old\",\n \"Anthropic Old\",\n \"Anthropic\",\n EntryStatus::Old,\n \"sk-anthropic-old\",\n \"2026-03-19T10:00:00Z\",\n ),\n sample_entry(\n \"entry-groq-revoked\",\n \"Groq Revoked\",\n \"Groq\",\n EntryStatus::Revoked,\n \"sk-groq-revoked\",\n \"2026-03-19T10:00:00Z\",\n ),\n ]);\n\n let export = render_export(&payload, &[], \"openClawBundle\").unwrap();\n let json: Value = serde_json::from_str(&export).unwrap();\n\n assert_eq!(\n json.get(\"format\")\n .and_then(Value::as_str)\n .unwrap_or_default(),\n \"codexvault.openclaw.bundle\"\n );\n assert!(json\n .get(\"env\")\n .and_then(|env| env.get(\"OPENAI_API_KEY\"))\n .is_some());\n assert!(json\n .get(\"env\")\n .and_then(|env| env.get(\"ANTHROPIC_API_KEY\"))\n .is_some());\n assert!(json\n .get(\"env\")\n .and_then(|env| env.get(\"GROQ_API_KEY\"))\n .is_none());\n assert!(json\n .get(\"providers\")\n .and_then(Value::as_array)\n .is_some_and(|items| !items.is_empty()));\n assert!(json\n .get(\"skipped\")\n .and_then(Value::as_array)\n .is_some_and(|items| !items.is_empty()));\n }\n}\n"}}}}