create-ekka-desktop-app 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ekka-desktop-app",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Create an EKKA desktop app with built-in demo backend. No setup required.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@ mod node_runner;
19
19
  mod node_vault_crypto;
20
20
  mod node_vault_store;
21
21
  mod ops;
22
+ mod security_epoch;
22
23
  mod state;
23
24
  mod types;
24
25
 
@@ -31,7 +32,8 @@ use tauri::Manager;
31
32
 
32
33
  fn main() {
33
34
  // Load .env.local for development (before anything else)
34
- // This provides EKKA_SECURITY_EPOCH and other dev-time overrides.
35
+ // This provides optional dev-time overrides (EKKA_SECURITY_EPOCH, etc).
36
+ // Note: EKKA_SECURITY_EPOCH is optional - epoch is resolved from marker file by default.
35
37
  // Note: ENGINE_GRANT_VERIFY_KEY_B64 is now fetched from /.well-known/ekka-configuration
36
38
  if let Err(_) = dotenvy::from_filename(".env.local") {
37
39
  // Also try parent directory (when running from src-tauri)
@@ -63,14 +65,6 @@ fn main() {
63
65
  // Attempt to spawn engine process
64
66
  tracing::info!(op = "desktop.startup", "EKKA Desktop starting");
65
67
 
66
- // Log security epoch status (still needed for home bootstrap)
67
- let security_epoch_set = std::env::var("EKKA_SECURITY_EPOCH").is_ok();
68
- tracing::info!(
69
- op = "desktop.required_env.loaded",
70
- EKKA_SECURITY_EPOCH = security_epoch_set,
71
- "Security epoch env var status"
72
- );
73
-
74
68
  // Log build-time baked engine URL presence (not the URL itself)
75
69
  let engine_url_baked = option_env!("EKKA_ENGINE_URL").is_some();
76
70
  tracing::info!(
@@ -16,6 +16,7 @@ use crate::node_vault_store::{
16
16
  delete_node_secret, has_node_secret, read_node_secret, write_node_secret,
17
17
  SECRET_ID_NODE_CREDENTIALS,
18
18
  };
19
+ use crate::security_epoch::resolve_security_epoch;
19
20
  use chrono::{DateTime, Utc};
20
21
  use serde::{Deserialize, Serialize};
21
22
  use std::sync::RwLock;
@@ -134,17 +135,6 @@ impl std::fmt::Display for CredentialsError {
134
135
 
135
136
  impl std::error::Error for CredentialsError {}
136
137
 
137
- // =============================================================================
138
- // Helper: Get epoch for vault operations
139
- // =============================================================================
140
-
141
- /// Get the security epoch from environment
142
- fn get_security_epoch() -> u32 {
143
- std::env::var("EKKA_SECURITY_EPOCH")
144
- .ok()
145
- .and_then(|s| s.parse().ok())
146
- .unwrap_or(1)
147
- }
148
138
 
149
139
  // =============================================================================
150
140
  // Core Functions
@@ -170,7 +160,7 @@ pub fn store_credentials(node_id: &Uuid, node_secret: &str) -> Result<(), Creden
170
160
  // Initialize home if needed
171
161
  let bootstrap = initialize_home().map_err(|e| CredentialsError::VaultError(e))?;
172
162
  let home = bootstrap.home_path();
173
- let epoch = get_security_epoch();
163
+ let epoch = resolve_security_epoch(home);
174
164
 
175
165
  // Store node_id + node_secret as JSON
176
166
  let creds = NodeCredentials {
@@ -202,7 +192,7 @@ pub fn store_credentials(node_id: &Uuid, node_secret: &str) -> Result<(), Creden
202
192
  /// * `Err(CredentialsError)` if not found or invalid
203
193
  pub fn load_credentials() -> Result<(Uuid, String), CredentialsError> {
204
194
  let home = resolve_home_path().map_err(|e| CredentialsError::VaultError(e))?;
205
- let epoch = get_security_epoch();
195
+ let epoch = resolve_security_epoch(&home);
206
196
 
207
197
  // Key derivation uses device_secret + epoch only (not node_id)
208
198
  let plaintext = read_node_secret(&home, epoch, SECRET_ID_NODE_CREDENTIALS)
@@ -0,0 +1,128 @@
1
+ //! Security Epoch Resolution
2
+ //!
3
+ //! Resolves the security epoch used for vault key derivation.
4
+ //! Resolution order:
5
+ //! 1. EKKA_SECURITY_EPOCH env var (override for dev/testing)
6
+ //! 2. Marker file epoch_seen field (canonical source)
7
+ //! 3. Default to 1 (new installations)
8
+
9
+ use std::path::Path;
10
+ use std::sync::OnceLock;
11
+
12
+ /// Cached resolved epoch (env var source only - marker is read fresh)
13
+ static ENV_EPOCH_CACHE: OnceLock<Option<u32>> = OnceLock::new();
14
+
15
+ /// Source of the resolved epoch
16
+ #[derive(Debug, Clone, Copy, PartialEq)]
17
+ pub enum EpochSource {
18
+ Env,
19
+ Marker,
20
+ Default,
21
+ }
22
+
23
+ impl std::fmt::Display for EpochSource {
24
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25
+ match self {
26
+ EpochSource::Env => write!(f, "env"),
27
+ EpochSource::Marker => write!(f, "marker"),
28
+ EpochSource::Default => write!(f, "default"),
29
+ }
30
+ }
31
+ }
32
+
33
+ /// Resolve security epoch from env var
34
+ fn try_env_epoch() -> Option<u32> {
35
+ *ENV_EPOCH_CACHE.get_or_init(|| {
36
+ std::env::var("EKKA_SECURITY_EPOCH")
37
+ .ok()
38
+ .and_then(|s| s.parse().ok())
39
+ })
40
+ }
41
+
42
+ /// Read epoch_seen from marker file
43
+ fn try_marker_epoch(home: &Path) -> Option<u32> {
44
+ let marker_path = home.join(".ekka-marker.json");
45
+ let content = std::fs::read_to_string(&marker_path).ok()?;
46
+ let marker: serde_json::Value = serde_json::from_str(&content).ok()?;
47
+ marker.get("epoch_seen").and_then(|v| v.as_u64()).map(|v| v as u32)
48
+ }
49
+
50
+ /// Resolve security epoch with precedence: env > marker > default
51
+ ///
52
+ /// Logs the resolution once per unique (home, source) combination.
53
+ pub fn resolve_security_epoch(home: &Path) -> u32 {
54
+ let (epoch, source) = resolve_with_source(home);
55
+
56
+ // Log resolution (only first time per call site in practice)
57
+ tracing::info!(
58
+ op = "security_epoch.resolved",
59
+ source = %source,
60
+ value = epoch,
61
+ "Security epoch resolved"
62
+ );
63
+
64
+ epoch
65
+ }
66
+
67
+ /// Resolve epoch and return both value and source
68
+ pub fn resolve_with_source(home: &Path) -> (u32, EpochSource) {
69
+ // 1. Check env var override
70
+ if let Some(epoch) = try_env_epoch() {
71
+ return (epoch, EpochSource::Env);
72
+ }
73
+
74
+ // 2. Read from marker file
75
+ if let Some(epoch) = try_marker_epoch(home) {
76
+ return (epoch, EpochSource::Marker);
77
+ }
78
+
79
+ // 3. Default
80
+ (1, EpochSource::Default)
81
+ }
82
+
83
+ #[cfg(test)]
84
+ mod tests {
85
+ use super::*;
86
+ use tempfile::TempDir;
87
+ use std::fs;
88
+
89
+ #[test]
90
+ fn test_marker_epoch_read() {
91
+ let temp = TempDir::new().unwrap();
92
+ let marker = serde_json::json!({
93
+ "schema_version": "1.0",
94
+ "app_name": "test",
95
+ "instance_id": "00000000-0000-0000-0000-000000000000",
96
+ "device_id_fingerprint": "sha256:test",
97
+ "created_at": "2024-01-01T00:00:00Z",
98
+ "last_seen_at": "2024-01-01T00:00:00Z",
99
+ "epoch_seen": 42,
100
+ "storage_layout_version": "v1"
101
+ });
102
+ fs::write(temp.path().join(".ekka-marker.json"), marker.to_string()).unwrap();
103
+
104
+ let epoch = try_marker_epoch(temp.path());
105
+ assert_eq!(epoch, Some(42));
106
+ }
107
+
108
+ #[test]
109
+ fn test_default_when_no_marker() {
110
+ let temp = TempDir::new().unwrap();
111
+ let (epoch, source) = resolve_with_source(temp.path());
112
+ assert_eq!(epoch, 1);
113
+ assert_eq!(source, EpochSource::Default);
114
+ }
115
+
116
+ #[test]
117
+ fn test_marker_source() {
118
+ let temp = TempDir::new().unwrap();
119
+ let marker = serde_json::json!({
120
+ "epoch_seen": 5
121
+ });
122
+ fs::write(temp.path().join(".ekka-marker.json"), marker.to_string()).unwrap();
123
+
124
+ let (epoch, source) = resolve_with_source(temp.path());
125
+ assert_eq!(epoch, 5);
126
+ assert_eq!(source, EpochSource::Marker);
127
+ }
128
+ }