create-ekka-desktop-app 0.4.0 → 0.4.1

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.
Files changed (49) hide show
  1. package/bin/cli.js +1 -5
  2. package/package.json +1 -1
  3. package/template/src/demo/DemoApp.tsx +0 -44
  4. package/template/src/demo/layout/Sidebar.tsx +2 -13
  5. package/template/src/demo/pages/LoginPage.tsx +1 -2
  6. package/template/src/demo/pages/SystemPage.tsx +1 -1
  7. package/template/src/ekka/backend/demo.ts +1 -1
  8. package/template/src/ekka/constants.ts +0 -4
  9. package/template/src/ekka/index.ts +0 -2
  10. package/template/src/ekka/internal/backend.ts +8 -8
  11. package/template/src/ekka/ops/index.ts +1 -2
  12. package/template/src/ekka/utils/index.ts +0 -1
  13. package/template/src-tauri/Cargo.toml +2 -0
  14. package/template/src-tauri/crates/ekka-desktop-core/Cargo.toml +30 -0
  15. package/template/src-tauri/crates/ekka-desktop-core/build.rs +42 -0
  16. package/template/src-tauri/crates/ekka-desktop-core/src/bootstrap.rs +39 -0
  17. package/template/src-tauri/crates/ekka-desktop-core/src/config.rs +32 -0
  18. package/template/src-tauri/crates/ekka-desktop-core/src/device_secret.rs +74 -0
  19. package/template/src-tauri/crates/ekka-desktop-core/src/main.rs +1225 -0
  20. package/template/src-tauri/crates/ekka-desktop-core/src/node_credentials.rs +413 -0
  21. package/template/src-tauri/crates/ekka-desktop-core/src/node_vault_crypto.rs +57 -0
  22. package/template/src-tauri/crates/ekka-desktop-core/src/node_vault_store.rs +198 -0
  23. package/template/src-tauri/crates/ekka-desktop-core/src/security_epoch.rs +80 -0
  24. package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
  25. package/template/src-tauri/src/commands.rs +137 -958
  26. package/template/src-tauri/src/config.rs +4 -44
  27. package/template/src-tauri/src/core_process.rs +335 -0
  28. package/template/src-tauri/src/engine_process.rs +103 -0
  29. package/template/src-tauri/src/handlers/home.rs +1 -32
  30. package/template/src-tauri/src/main.rs +240 -153
  31. package/template/src-tauri/src/node_auth.rs +2 -748
  32. package/template/src-tauri/src/node_credentials.rs +2 -201
  33. package/template/src-tauri/src/node_runner.rs +2 -55
  34. package/template/src-tauri/src/node_vault_crypto.rs +0 -33
  35. package/template/src-tauri/src/node_vault_store.rs +1 -150
  36. package/template/src-tauri/src/ops/mod.rs +0 -2
  37. package/template/src-tauri/src/state.rs +7 -63
  38. package/template/src-tauri/src/types.rs +1 -23
  39. package/template/src-tauri/src/updater.rs +215 -0
  40. package/template/src-tauri/tauri.conf.json +9 -0
  41. package/template/src/demo/pages/DocGenPage.tsx +0 -731
  42. package/template/src/ekka/config.ts +0 -48
  43. package/template/src/ekka/ops/debug.ts +0 -68
  44. package/template/src/ekka/ops/executionRuns.ts +0 -147
  45. package/template/src/ekka/ops/workflowRuns.ts +0 -119
  46. package/template/src/ekka/utils/idempotency.ts +0 -14
  47. package/template/src-tauri/src/ops/home.rs +0 -251
  48. package/template/src-tauri/src/ops/runtime.rs +0 -21
  49. package/template/src-tauri/src/well_known.rs +0 -83
package/bin/cli.js CHANGED
@@ -225,14 +225,10 @@ Creating EKKA desktop app in ${targetDir}...
225
225
  cargoContent = cargoContent.replace(/^description = ".*"$/m, `description = "${config.app.name}"`);
226
226
  writeFileSync(cargoPath, cargoContent);
227
227
 
228
- // Update src-tauri/tauri.conf.json
228
+ // Update src-tauri/tauri.conf.json identifier
229
229
  const tauriConfPath = join(targetDir, 'src-tauri', 'tauri.conf.json');
230
230
  const tauriConf = JSON.parse(readFileSync(tauriConfPath, 'utf8'));
231
231
  tauriConf.identifier = config.app.identifier;
232
- tauriConf.productName = config.app.name;
233
- if (tauriConf.app?.windows?.[0]) {
234
- tauriConf.app.windows[0].title = config.app.name;
235
- }
236
232
  writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n');
237
233
 
238
234
  console.log(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ekka-desktop-app",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Create an EKKA desktop app with built-in demo backend. No setup required.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,6 @@ import { SystemPage } from './pages/SystemPage';
19
19
  import { AuditLogPage } from './pages/AuditLogPage';
20
20
  import { PathPermissionsPage } from './pages/PathPermissionsPage';
21
21
  import { VaultPage } from './pages/VaultPage';
22
- import { DocGenPage } from './pages/DocGenPage';
23
22
  import { RunnerPage } from './pages/RunnerPage';
24
23
  import { LoginPage } from './pages/LoginPage';
25
24
  import { HomeSetupPage } from './pages/HomeSetupPage';
@@ -35,34 +34,6 @@ interface DemoState {
35
34
  error: string | null;
36
35
  }
37
36
 
38
- // DocGen state persisted across tab switches
39
- interface DocGenPersistedState {
40
- runId: string | null;
41
- folder: string | null;
42
- }
43
-
44
- const DOCGEN_STORAGE_KEY = 'ekka.docgen.state';
45
-
46
- function loadDocGenState(): DocGenPersistedState {
47
- try {
48
- const saved = localStorage.getItem(DOCGEN_STORAGE_KEY);
49
- if (saved) {
50
- return JSON.parse(saved) as DocGenPersistedState;
51
- }
52
- } catch {
53
- // Ignore parse errors
54
- }
55
- return { runId: null, folder: null };
56
- }
57
-
58
- function saveDocGenState(state: DocGenPersistedState): void {
59
- try {
60
- localStorage.setItem(DOCGEN_STORAGE_KEY, JSON.stringify(state));
61
- } catch {
62
- // Ignore storage errors
63
- }
64
- }
65
-
66
37
  export function DemoApp(): ReactElement {
67
38
  const [selectedPage, setSelectedPage] = useState<Page>('path-permissions');
68
39
  const [darkMode, setDarkMode] = useState<boolean>(() => {
@@ -79,14 +50,6 @@ export function DemoApp(): ReactElement {
79
50
  error: null,
80
51
  });
81
52
 
82
- // DocGen state - persisted to localStorage
83
- const [docGenState, setDocGenState] = useState<DocGenPersistedState>(loadDocGenState);
84
-
85
- const handleDocGenStateChange = (newState: DocGenPersistedState) => {
86
- setDocGenState(newState);
87
- saveDocGenState(newState);
88
- };
89
-
90
53
  useEffect(() => {
91
54
  void initializeApp();
92
55
  }, []);
@@ -287,13 +250,6 @@ export function DemoApp(): ReactElement {
287
250
  {state.error && <div style={errorStyle}>{state.error}</div>}
288
251
  {selectedPage === 'path-permissions' && <PathPermissionsPage darkMode={darkMode} />}
289
252
  {selectedPage === 'vault' && <VaultPage darkMode={darkMode} />}
290
- {selectedPage === 'doc-gen' && (
291
- <DocGenPage
292
- darkMode={darkMode}
293
- persistedState={docGenState}
294
- onStateChange={handleDocGenStateChange}
295
- />
296
- )}
297
253
  {selectedPage === 'runner' && <RunnerPage darkMode={darkMode} />}
298
254
  {selectedPage === 'audit-log' && <AuditLogPage darkMode={darkMode} />}
299
255
  {selectedPage === 'system' && <SystemPage darkMode={darkMode} />}
@@ -4,9 +4,8 @@
4
4
  */
5
5
 
6
6
  import { type CSSProperties, type ReactElement } from 'react';
7
- import branding from '../../../branding/app.json';
8
7
 
9
- export type Page = 'audit-log' | 'path-permissions' | 'vault' | 'doc-gen' | 'runner' | 'system';
8
+ export type Page = 'audit-log' | 'path-permissions' | 'vault' | 'runner' | 'system';
10
9
 
11
10
  interface SidebarProps {
12
11
  selectedPage: Page;
@@ -119,14 +118,13 @@ export function Sidebar({ selectedPage, onNavigate, darkMode }: SidebarProps): R
119
118
  return (
120
119
  <aside style={styles.sidebar}>
121
120
  <div style={styles.logo}>
122
- <span style={styles.logoText}>{branding.name}</span>
121
+ <span style={styles.logoText}>EKKA Desktop</span>
123
122
  </div>
124
123
 
125
124
  <nav style={styles.nav}>
126
125
  <div style={styles.sectionLabel}>Tools</div>
127
126
  <NavButton page="path-permissions" label="Path Permissions" icon={<PathIcon />} />
128
127
  <NavButton page="vault" label="Vault" icon={<VaultIcon />} />
129
- <NavButton page="doc-gen" label="Generate Docs" icon={<DocGenIcon />} />
130
128
  <NavButton page="runner" label="Runner" icon={<RunnerIcon />} />
131
129
  <NavButton page="audit-log" label="Audit Log" icon={<AuditIcon />} />
132
130
  </nav>
@@ -173,15 +171,6 @@ function SystemIcon(): ReactElement {
173
171
  );
174
172
  }
175
173
 
176
- function DocGenIcon(): ReactElement {
177
- return (
178
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style={{ opacity: 0.7 }}>
179
- <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zM9.5 4V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-3.5z" />
180
- <path d="M4.5 12.5A.5.5 0 0 1 5 12h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 10h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 8h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5z" />
181
- </svg>
182
- );
183
- }
184
-
185
174
  function RunnerIcon(): ReactElement {
186
175
  return (
187
176
  <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style={{ opacity: 0.7 }}>
@@ -6,7 +6,6 @@
6
6
 
7
7
  import { useState, type ReactElement, type FormEvent } from 'react';
8
8
  import { ekka } from '../../ekka';
9
- import branding from '../../../branding/app.json';
10
9
 
11
10
  interface LoginPageProps {
12
11
  onLoginSuccess: () => void;
@@ -135,7 +134,7 @@ export function LoginPage({ onLoginSuccess, darkMode }: LoginPageProps): ReactEl
135
134
  return (
136
135
  <div style={styles.container}>
137
136
  <div style={styles.card}>
138
- <div style={styles.logo}>{branding.name}</div>
137
+ <div style={styles.logo}>EKKA Desktop</div>
139
138
  <div style={styles.subtitle}>Sign in to continue</div>
140
139
 
141
140
  <form style={styles.form} onSubmit={handleSubmit}>
@@ -383,7 +383,7 @@ export function SystemPage({ darkMode }: SystemPageProps): ReactElement {
383
383
  <div style={styles.row}>
384
384
  <span style={styles.label}>Environment</span>
385
385
  <span style={{ ...styles.badge, ...styles.badgeBlue }}>
386
- {info.runtime?.runtime === 'tauri' ? 'Desktop (Tauri)' : 'Web Browser'}
386
+ {info.runtime?.runtime === 'ekka-bridge' ? 'EKKA Desktop' : 'Web Browser'}
387
387
  </span>
388
388
  </div>
389
389
  <div style={styles.row}>
@@ -19,7 +19,7 @@ const DEMO_HOME_PATH = `~/.local/share/${slugify(branding.name) || 'ekka-desktop
19
19
 
20
20
  /**
21
21
  * Demo backend using in-memory storage.
22
- * Used when Tauri engine is not available.
22
+ * Used when EKKA Bridge is not available.
23
23
  */
24
24
  export class DemoBackend implements Backend {
25
25
  private connected = false;
@@ -33,10 +33,6 @@ export const OPS = {
33
33
  RUNNER_STATUS: 'runner.status',
34
34
  RUNNER_TASK_STATS: 'runner.taskStats',
35
35
 
36
- // Workflow Runs (proxied via Rust)
37
- WORKFLOW_RUNS_CREATE: 'workflowRuns.create',
38
- WORKFLOW_RUNS_GET: 'workflowRuns.get',
39
-
40
36
  // Auth (proxied via Rust)
41
37
  AUTH_LOGIN: 'auth.login',
42
38
  AUTH_REFRESH: 'auth.refresh',
@@ -501,8 +501,6 @@ export {
501
501
  formatExpiryInfo,
502
502
  } from './utils/time';
503
503
 
504
- export { generateIdempotencyKey } from './utils/idempotency';
505
-
506
504
  // =============================================================================
507
505
  // AUDIT (client-side event logging)
508
506
  // =============================================================================
@@ -18,7 +18,7 @@ export type TransportMode = 'unknown' | 'engine' | 'demo';
18
18
  * SmartBackend - single backend that auto-detects engine vs demo.
19
19
  *
20
20
  * On connect():
21
- * - Tries to connect to Tauri engine
21
+ * - Tries to connect to EKKA Bridge
22
22
  * - If successful: engine mode
23
23
  * - If fails: demo mode (in-memory)
24
24
  */
@@ -34,7 +34,7 @@ class SmartBackend {
34
34
  async connect(): Promise<void> {
35
35
  if (this.connected) return;
36
36
 
37
- // Try engine first (only works in Tauri with engine present)
37
+ // Try engine first (only works in EKKA Bridge with engine present)
38
38
  try {
39
39
  const { invoke } = await import('@tauri-apps/api/core');
40
40
  await invoke('engine_connect');
@@ -88,7 +88,7 @@ class SmartBackend {
88
88
  * Send a request to the backend.
89
89
  */
90
90
  async request(req: EngineRequest): Promise<EngineResponse> {
91
- // LOCAL-ONLY OPERATIONS: Always route to Tauri, never to demo backend
91
+ // LOCAL-ONLY OPERATIONS: Always route to Bridge, never to demo backend
92
92
  // These are desktop-specific operations that must be handled by Rust handlers
93
93
  const localOnlyOps = [
94
94
  'setup.status',
@@ -99,17 +99,17 @@ class SmartBackend {
99
99
 
100
100
  const isLocalOnlyOp = localOnlyOps.includes(req.op);
101
101
 
102
- // Local-only ops ALWAYS go to Tauri - regardless of connection state or mode
102
+ // Local-only ops ALWAYS go to Bridge - regardless of connection state or mode
103
103
  // This ensures setup operations never accidentally route to demo backend
104
104
  if (isLocalOnlyOp) {
105
- console.log(`[ts.op.dispatch] op=${req.op} backend=tauri (local-only)`);
105
+ console.log(`[ts.op.dispatch] op=${req.op} backend=bridge (local-only)`);
106
106
  try {
107
107
  const { invoke } = await import('@tauri-apps/api/core');
108
108
  return await invoke<EngineResponse>('engine_request', { req });
109
109
  } catch (e) {
110
- const message = e instanceof Error ? e.message : 'Tauri not available';
111
- console.error(`[ts.op.dispatch] op=${req.op} backend=tauri FAILED: ${message}`);
112
- return err('TAURI_NOT_READY', message);
110
+ const message = e instanceof Error ? e.message : 'Bridge not available';
111
+ console.error(`[ts.op.dispatch] op=${req.op} backend=bridge FAILED: ${message}`);
112
+ return err('BRIDGE_NOT_READY', message);
113
113
  }
114
114
  }
115
115
 
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Operations
3
3
  *
4
- * Mirrors Rust src-tauri/src/ops/ and handlers/
4
+ * Mirrors Rust Bridge ops/ and handlers/
5
5
  */
6
6
 
7
7
  export * as auth from './auth';
8
- export * as debug from './debug';
9
8
  export * as home from './home';
10
9
  export * as paths from './paths';
11
10
  export * as runtime from './runtime';
@@ -4,4 +4,3 @@
4
4
  */
5
5
 
6
6
  export * from './time';
7
- export * from './idempotency';
@@ -15,6 +15,7 @@ serde_json = "1"
15
15
  [dependencies]
16
16
  tauri = { version = "2", features = [] }
17
17
  tauri-plugin-dialog = "2"
18
+ tauri-plugin-updater = "2"
18
19
  serde = { version = "1", features = ["derive"] }
19
20
  serde_json = "1"
20
21
  chrono = { version = "0.4", features = ["serde"] }
@@ -22,6 +23,7 @@ ekka-sdk-core = { path = "../../ekka-execution-node-sdk-rust/crates/framework/ek
22
23
  ekka-runner-core = { path = "../../ekka-execution-node-sdk-rust/crates/framework/ekka-runner-core" }
23
24
  ekka-runner-local = { path = "../../ekka-execution-node-sdk-rust/crates/apps/ekka-runner-local" }
24
25
  reqwest = { version = "0.11", features = ["blocking", "json"] }
26
+ http = "1"
25
27
  uuid = { version = "1.0", features = ["v4"] }
26
28
  tokio = { version = "1", features = ["sync"] }
27
29
  tracing = "0.1"
@@ -0,0 +1,30 @@
1
+ [package]
2
+ name = "ekka-desktop-core"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "EKKA Desktop Core - Security-critical logic process (JSON-RPC over stdio)"
6
+
7
+ # Prevent inheriting parent workspace
8
+ [workspace]
9
+
10
+ [build-dependencies]
11
+ serde_json = "1"
12
+
13
+ [dependencies]
14
+ serde = { version = "1", features = ["derive"] }
15
+ serde_json = "1"
16
+ chrono = { version = "0.4", features = ["serde"] }
17
+ uuid = { version = "1.0", features = ["v4"] }
18
+ reqwest = { version = "0.11", features = ["blocking", "json"] }
19
+ tracing = "0.1"
20
+ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
21
+ rand = "0.8"
22
+ base64 = "0.22"
23
+ hex = "0.4"
24
+ anyhow = "1.0"
25
+
26
+ # SDK dependencies (only the primitives we need)
27
+ ekka-sdk-core = { path = "../../../../ekka-execution-node-sdk-rust/crates/framework/ekka-sdk-core" }
28
+
29
+ [dev-dependencies]
30
+ tempfile = "3"
@@ -0,0 +1,42 @@
1
+ use std::fs;
2
+
3
+ fn main() {
4
+ // Read app.config.json (same source as Bridge build.rs)
5
+ // Walk up to find app.config.json from crates/ekka-desktop-core/
6
+ let config_path = "../../../app.config.json";
7
+ println!("cargo:rerun-if-changed={}", config_path);
8
+
9
+ let config_str = fs::read_to_string(config_path).unwrap_or_else(|_| {
10
+ panic!(
11
+ "\n\nBUILD ERROR: app.config.json not found at {}\n\
12
+ Desktop Core must be built from within the ekka-desktop-app tree.\n\n",
13
+ config_path
14
+ )
15
+ });
16
+
17
+ let config: serde_json::Value = serde_json::from_str(&config_str).unwrap_or_else(|e| {
18
+ panic!("\n\nBUILD ERROR: Invalid app.config.json: {}\n\n", e)
19
+ });
20
+
21
+ let app = config.get("app").expect("app.config.json missing 'app' section");
22
+ let storage = config.get("storage").expect("app.config.json missing 'storage' section");
23
+ let engine = config.get("engine").expect("app.config.json missing 'engine' section");
24
+
25
+ let app_name = app.get("name").and_then(|v| v.as_str())
26
+ .expect("app.config.json: app.name is required");
27
+ let app_slug = app.get("slug").and_then(|v| v.as_str())
28
+ .expect("app.config.json: app.slug is required");
29
+ let home_folder = storage.get("homeFolderName").and_then(|v| v.as_str())
30
+ .expect("app.config.json: storage.homeFolderName is required");
31
+ let keychain_service = storage.get("keychainService").and_then(|v| v.as_str())
32
+ .expect("app.config.json: storage.keychainService is required");
33
+ let engine_url = engine.get("url").and_then(|v| v.as_str())
34
+ .expect("app.config.json: engine.url is required");
35
+
36
+ // Bake identical values as Bridge build.rs
37
+ println!("cargo:rustc-env=EKKA_APP_NAME={}", app_name);
38
+ println!("cargo:rustc-env=EKKA_APP_SLUG={}", app_slug);
39
+ println!("cargo:rustc-env=EKKA_HOME_FOLDER={}", home_folder);
40
+ println!("cargo:rustc-env=EKKA_KEYCHAIN_SERVICE={}", keychain_service);
41
+ println!("cargo:rustc-env=EKKA_ENGINE_URL={}", engine_url);
42
+ }
@@ -0,0 +1,39 @@
1
+ //! Home directory bootstrap
2
+ //!
3
+ //! Handles initialization and resolution of the EKKA home directory.
4
+ //! Mirrors Bridge bootstrap.rs exactly.
5
+
6
+ use crate::config;
7
+ use ekka_sdk_core::ekka_home_bootstrap::{BootstrapConfig, EpochSource, HomeBootstrap, HomeStrategy};
8
+ use std::path::PathBuf;
9
+
10
+ /// Standard bootstrap configuration - all values from app.config.json (baked at build time)
11
+ pub fn bootstrap_config() -> BootstrapConfig {
12
+ BootstrapConfig {
13
+ app_name: config::app_name().to_string(),
14
+ default_folder_name: config::home_folder().to_string(),
15
+ home_strategy: HomeStrategy::DataHome {
16
+ env_var: "EKKA_DATA_HOME".to_string(),
17
+ },
18
+ marker_filename: ".ekka-marker.json".to_string(),
19
+ keychain_service: config::keychain_service().to_string(),
20
+ subdirs: vec!["vault".to_string(), "db".to_string(), "tmp".to_string()],
21
+ epoch_source: EpochSource::EnvVar("EKKA_SECURITY_EPOCH".to_string()),
22
+ storage_layout_version: "v1".to_string(),
23
+ }
24
+ }
25
+
26
+ /// Resolve the home path without initializing
27
+ pub fn resolve_home_path() -> Result<PathBuf, String> {
28
+ let config = bootstrap_config();
29
+ let bootstrap = HomeBootstrap::new(config).map_err(|e| e.to_string())?;
30
+ Ok(bootstrap.home_path().to_path_buf())
31
+ }
32
+
33
+ /// Initialize home directory and return the bootstrap instance
34
+ pub fn initialize_home() -> Result<HomeBootstrap, String> {
35
+ let config = bootstrap_config();
36
+ let bootstrap = HomeBootstrap::new(config).map_err(|e| e.to_string())?;
37
+ bootstrap.initialize().map_err(|e| e.to_string())?;
38
+ Ok(bootstrap)
39
+ }
@@ -0,0 +1,32 @@
1
+ //! Compile-time app configuration
2
+ //!
3
+ //! All values are baked at build time from app.config.json.
4
+ //! Mirrors Bridge config.rs for the subset needed by Desktop Core.
5
+
6
+ #![allow(dead_code)]
7
+
8
+ macro_rules! baked_config {
9
+ ($name:ident, $env:literal) => {
10
+ pub fn $name() -> &'static str {
11
+ option_env!($env).expect(concat!(
12
+ $env,
13
+ " not baked at build time. Check build.rs and app.config.json"
14
+ ))
15
+ }
16
+ };
17
+ }
18
+
19
+ // App display name (e.g., "EKKA Desktop")
20
+ baked_config!(app_name, "EKKA_APP_NAME");
21
+
22
+ // App slug for machine use (e.g., "ekka-desktop")
23
+ baked_config!(app_slug, "EKKA_APP_SLUG");
24
+
25
+ // Home folder name (e.g., ".ekka-desktop")
26
+ baked_config!(home_folder, "EKKA_HOME_FOLDER");
27
+
28
+ // Keychain service identifier (e.g., "ai.ekka.desktop")
29
+ baked_config!(keychain_service, "EKKA_KEYCHAIN_SERVICE");
30
+
31
+ // EKKA Engine URL (e.g., "https://api.ekka.ai")
32
+ baked_config!(engine_url, "EKKA_ENGINE_URL");
@@ -0,0 +1,74 @@
1
+ //! Device Secret Management
2
+ //!
3
+ //! Generates and stores a device-bound secret for encrypting node-level data.
4
+ //! The device secret is stored as a 32-byte file with 0600 permissions.
5
+ //! This secret never leaves the device and is used to derive encryption keys.
6
+
7
+ use rand::RngCore;
8
+ use std::fs;
9
+ use std::io::{Read, Write};
10
+ use std::path::{Path, PathBuf};
11
+
12
+ #[cfg(unix)]
13
+ use std::os::unix::fs::PermissionsExt;
14
+
15
+ /// Device secret filename
16
+ const DEVICE_SECRET_FILENAME: &str = ".ekka-device-secret";
17
+
18
+ /// Device secret size in bytes (256 bits)
19
+ const DEVICE_SECRET_SIZE: usize = 32;
20
+
21
+ /// Get the path to the device secret file
22
+ pub fn device_secret_path(home: &Path) -> PathBuf {
23
+ home.join(DEVICE_SECRET_FILENAME)
24
+ }
25
+
26
+ /// Load or create the device secret
27
+ ///
28
+ /// If the secret file exists, reads exactly 32 bytes.
29
+ /// If not, generates 32 random bytes and writes with 0600 permissions.
30
+ ///
31
+ /// # Returns
32
+ /// * `Ok([u8; 32])` - The device secret bytes
33
+ /// * `Err` - If file operations fail
34
+ pub fn load_or_create_device_secret(home: &Path) -> anyhow::Result<[u8; 32]> {
35
+ let path = device_secret_path(home);
36
+
37
+ if path.exists() {
38
+ // Load existing secret
39
+ let mut file = fs::File::open(&path)?;
40
+ let mut secret = [0u8; DEVICE_SECRET_SIZE];
41
+ file.read_exact(&mut secret)?;
42
+
43
+ tracing::info!(
44
+ op = "device_secret.ready",
45
+ created = false,
46
+ "Device secret loaded"
47
+ );
48
+
49
+ Ok(secret)
50
+ } else {
51
+ // Generate new secret
52
+ let mut secret = [0u8; DEVICE_SECRET_SIZE];
53
+ rand::thread_rng().fill_bytes(&mut secret);
54
+
55
+ // Write with secure permissions
56
+ let mut file = fs::File::create(&path)?;
57
+ file.write_all(&secret)?;
58
+
59
+ // Set 0600 permissions on Unix
60
+ #[cfg(unix)]
61
+ {
62
+ let perms = fs::Permissions::from_mode(0o600);
63
+ fs::set_permissions(&path, perms)?;
64
+ }
65
+
66
+ tracing::info!(
67
+ op = "device_secret.ready",
68
+ created = true,
69
+ "Device secret created"
70
+ );
71
+
72
+ Ok(secret)
73
+ }
74
+ }