create-ekka-desktop-app 0.2.3 → 0.3.0

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/bin/cli.js CHANGED
@@ -3,70 +3,251 @@
3
3
  import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { resolve, join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { createInterface } from 'node:readline';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
  const templateDir = resolve(__dirname, '..', 'template');
9
10
 
10
- const projectName = process.argv[2];
11
+ // Parse command line arguments
12
+ const args = process.argv.slice(2);
13
+ let projectName = null;
14
+ let configFile = null;
15
+ let appName = null;
16
+ let appSlug = null;
17
+ let engineUrl = null;
18
+ let orgPrefix = 'ai.ekka';
19
+
20
+ for (let i = 0; i < args.length; i++) {
21
+ const arg = args[i];
22
+ if (arg === '--config' || arg === '-c') {
23
+ configFile = args[++i];
24
+ } else if (arg === '--name' || arg === '-n') {
25
+ appName = args[++i];
26
+ } else if (arg === '--slug' || arg === '-s') {
27
+ appSlug = args[++i];
28
+ } else if (arg === '--engine-url' || arg === '-e') {
29
+ engineUrl = args[++i];
30
+ } else if (arg === '--org' || arg === '-o') {
31
+ orgPrefix = args[++i];
32
+ } else if (arg === '--help' || arg === '-h') {
33
+ console.log(`
34
+ Usage: npx create-ekka-desktop-app [project-dir] [options]
35
+
36
+ Options:
37
+ -c, --config <file> Use config file (skips all prompts)
38
+ -n, --name <name> App display name
39
+ -s, --slug <slug> App identifier (lowercase, hyphens only)
40
+ -e, --engine-url <url> EKKA Engine URL (required)
41
+ -o, --org <prefix> Organization prefix (default: ai.ekka)
42
+ -h, --help Show this help
43
+
44
+ Examples:
45
+ npx create-ekka-desktop-app my-app
46
+ npx create-ekka-desktop-app my-app --engine-url https://api.ekka.ai
47
+ npx create-ekka-desktop-app my-app --config ./app.config.json
48
+ `);
49
+ process.exit(0);
50
+ } else if (!arg.startsWith('-') && !projectName) {
51
+ projectName = arg;
52
+ }
53
+ }
54
+
55
+ // Helper to prompt for input
56
+ async function prompt(question, defaultValue = '') {
57
+ const rl = createInterface({
58
+ input: process.stdin,
59
+ output: process.stdout
60
+ });
61
+
62
+ return new Promise((resolve) => {
63
+ const displayDefault = defaultValue ? ` (${defaultValue})` : '';
64
+ rl.question(`${question}${displayDefault}: `, (answer) => {
65
+ rl.close();
66
+ resolve(answer.trim() || defaultValue);
67
+ });
68
+ });
69
+ }
11
70
 
12
- if (!projectName) {
13
- console.error('Usage: npx create-ekka-desktop-app <project-name>');
14
- process.exit(1);
71
+ // Helper to convert to title case
72
+ function titleCase(str) {
73
+ return str
74
+ .split('-')
75
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
76
+ .join(' ');
15
77
  }
16
78
 
17
- const targetDir = resolve(process.cwd(), projectName);
79
+ // Helper to slugify
80
+ function slugify(str) {
81
+ return str
82
+ .toLowerCase()
83
+ .replace(/[^a-z0-9-]/g, '-')
84
+ .replace(/-+/g, '-')
85
+ .replace(/^-|-$/g, '');
86
+ }
18
87
 
19
- if (existsSync(targetDir)) {
20
- console.error(`Error: Directory "${projectName}" already exists.`);
21
- process.exit(1);
88
+ // Validate slug
89
+ function isValidSlug(slug) {
90
+ return /^[a-z][a-z0-9-]*$/.test(slug);
22
91
  }
23
92
 
24
- console.log(`Creating EKKA desktop app in ${targetDir}...`);
25
-
26
- // Copy template
27
- mkdirSync(targetDir, { recursive: true });
28
- cpSync(templateDir, targetDir, { recursive: true });
29
-
30
- // Update package.json with project name
31
- const pkgPath = join(targetDir, 'package.json');
32
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
33
- pkg.name = projectName;
34
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
35
-
36
- // Update branding/app.json with project name
37
- const brandingPath = join(targetDir, 'branding', 'app.json');
38
- const branding = JSON.parse(readFileSync(brandingPath, 'utf8'));
39
- // Convert project-name to "Project Name" for display
40
- const displayName = projectName
41
- .split('-')
42
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
43
- .join(' ');
44
- // Convert project-name to ai.ekka.projectname for bundleId
45
- const bundleId = `ai.ekka.${projectName.replace(/-/g, '')}`;
46
- branding.name = displayName;
47
- branding.bundleId = bundleId;
48
- writeFileSync(brandingPath, JSON.stringify(branding, null, 2) + '\n');
49
-
50
- // Update src-tauri/Cargo.toml crate name
51
- const cargoPath = join(targetDir, 'src-tauri', 'Cargo.toml');
52
- let cargoContent = readFileSync(cargoPath, 'utf8');
53
- cargoContent = cargoContent.replace(/^name = ".*"$/m, `name = "${projectName}"`);
54
- cargoContent = cargoContent.replace(/^description = ".*"$/m, `description = "${displayName}"`);
55
- writeFileSync(cargoPath, cargoContent);
56
-
57
- console.log(`
58
- Done! To get started:
93
+ // Main
94
+ async function main() {
95
+ console.log(`
96
+ ╔══════════════════════════════════════════════════════════════╗
97
+ ║ CREATE EKKA DESKTOP APP ║
98
+ ╚══════════════════════════════════════════════════════════════╝
99
+ `);
100
+
101
+ // If config file provided, load it and skip prompts
102
+ let config = null;
103
+ if (configFile) {
104
+ if (!existsSync(configFile)) {
105
+ console.error(`Error: Config file not found: ${configFile}`);
106
+ process.exit(1);
107
+ }
108
+ try {
109
+ config = JSON.parse(readFileSync(configFile, 'utf8'));
110
+ console.log(`Using config from: ${configFile}\n`);
111
+ } catch (e) {
112
+ console.error(`Error: Invalid JSON in config file: ${e.message}`);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ // Get project directory
118
+ if (!projectName) {
119
+ projectName = await prompt('Project directory', 'my-ekka-app');
120
+ }
121
+
122
+ const targetDir = resolve(process.cwd(), projectName);
123
+
124
+ if (existsSync(targetDir)) {
125
+ console.error(`\nError: Directory "${projectName}" already exists.`);
126
+ process.exit(1);
127
+ }
128
+
129
+ // Build config from flags, prompts, or config file
130
+ if (!config) {
131
+ // Get app name
132
+ if (!appName) {
133
+ appName = await prompt('App display name', titleCase(projectName));
134
+ }
135
+
136
+ // Get app slug
137
+ if (!appSlug) {
138
+ const defaultSlug = slugify(projectName);
139
+ appSlug = await prompt('App identifier (lowercase, hyphens)', defaultSlug);
140
+ while (!isValidSlug(appSlug)) {
141
+ console.log(' ⚠ Must be lowercase letters, numbers, and hyphens only');
142
+ appSlug = await prompt('App identifier (lowercase, hyphens)', defaultSlug);
143
+ }
144
+ }
145
+
146
+ // Get engine URL
147
+ if (!engineUrl) {
148
+ engineUrl = await prompt('EKKA Engine URL', 'https://api.ekka.ai');
149
+ }
150
+
151
+ // Get org prefix (only prompt if not all values provided via flags)
152
+ if (orgPrefix === 'ai.ekka' && !process.argv.includes('--engine-url') && !process.argv.includes('-e')) {
153
+ const customOrg = await prompt('Organization prefix', 'ai.ekka');
154
+ if (customOrg) orgPrefix = customOrg;
155
+ }
156
+
157
+ config = {
158
+ app: {
159
+ name: appName,
160
+ slug: appSlug,
161
+ identifier: `${orgPrefix}.${appSlug.replace(/-/g, '')}`
162
+ },
163
+ storage: {
164
+ homeFolderName: `.${appSlug}`,
165
+ keychainService: `${orgPrefix}.${appSlug.replace(/-/g, '')}`
166
+ },
167
+ engine: {
168
+ url: engineUrl
169
+ }
170
+ };
171
+ }
172
+
173
+ // Validate config
174
+ if (!config.app?.slug) {
175
+ console.error('\nError: app.slug is required in config');
176
+ process.exit(1);
177
+ }
178
+ if (!config.engine?.url) {
179
+ console.error('\nError: engine.url is required in config');
180
+ process.exit(1);
181
+ }
182
+
183
+ // Derive missing values
184
+ config.app.name = config.app.name || titleCase(config.app.slug);
185
+ config.app.identifier = config.app.identifier || `ai.ekka.${config.app.slug.replace(/-/g, '')}`;
186
+ config.storage = config.storage || {};
187
+ config.storage.homeFolderName = config.storage.homeFolderName || `.${config.app.slug}`;
188
+ config.storage.keychainService = config.storage.keychainService || config.app.identifier;
189
+
190
+ console.log(`
191
+ Creating EKKA desktop app in ${targetDir}...
192
+
193
+ App Name: ${config.app.name}
194
+ App Slug: ${config.app.slug}
195
+ Identifier: ${config.app.identifier}
196
+ Home Folder: ~/${config.storage.homeFolderName}
197
+ Engine URL: ${config.engine.url}
198
+ `);
199
+
200
+ // Copy template
201
+ mkdirSync(targetDir, { recursive: true });
202
+ cpSync(templateDir, targetDir, { recursive: true });
203
+
204
+ // Write app.config.json
205
+ const appConfigPath = join(targetDir, 'app.config.json');
206
+ writeFileSync(appConfigPath, JSON.stringify(config, null, 2) + '\n');
207
+
208
+ // Update package.json with project name
209
+ const pkgPath = join(targetDir, 'package.json');
210
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
211
+ pkg.name = config.app.slug;
212
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
213
+
214
+ // Update branding/app.json
215
+ const brandingPath = join(targetDir, 'branding', 'app.json');
216
+ const branding = JSON.parse(readFileSync(brandingPath, 'utf8'));
217
+ branding.name = config.app.name;
218
+ branding.bundleId = config.app.identifier;
219
+ writeFileSync(brandingPath, JSON.stringify(branding, null, 2) + '\n');
220
+
221
+ // Update src-tauri/Cargo.toml crate name
222
+ const cargoPath = join(targetDir, 'src-tauri', 'Cargo.toml');
223
+ let cargoContent = readFileSync(cargoPath, 'utf8');
224
+ cargoContent = cargoContent.replace(/^name = ".*"$/m, `name = "${config.app.slug}"`);
225
+ cargoContent = cargoContent.replace(/^description = ".*"$/m, `description = "${config.app.name}"`);
226
+ writeFileSync(cargoPath, cargoContent);
227
+
228
+ // Update src-tauri/tauri.conf.json identifier
229
+ const tauriConfPath = join(targetDir, 'src-tauri', 'tauri.conf.json');
230
+ const tauriConf = JSON.parse(readFileSync(tauriConfPath, 'utf8'));
231
+ tauriConf.identifier = config.app.identifier;
232
+ writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n');
233
+
234
+ console.log(`
235
+ ╔══════════════════════════════════════════════════════════════╗
236
+ ║ SUCCESS! Your app is ready. ║
237
+ ╚══════════════════════════════════════════════════════════════╝
238
+
239
+ To get started:
59
240
 
60
241
  cd ${projectName}
61
242
  npm install
243
+ npm run tauri:dev
62
244
 
63
- Development:
64
- npm start # Web (browser)
65
- npm run tauri:dev # Desktop (native window)
245
+ Configuration:
246
+ Edit app.config.json to change app identity or engine URL.
66
247
 
67
248
  Build:
68
- npm run tauri:build # Create distributable app
69
-
70
- Edit src/app/App.tsx to build your UI.
71
- Delete src/demo/ when ready.
249
+ npm run tauri:build
72
250
  `);
251
+ }
252
+
253
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ekka-desktop-app",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Create an EKKA desktop app with built-in demo backend. No setup required.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,14 @@
1
+ {
2
+ "app": {
3
+ "name": "EKKA Desktop",
4
+ "slug": "ekka-desktop",
5
+ "identifier": "ai.ekka.desktop"
6
+ },
7
+ "storage": {
8
+ "homeFolderName": ".ekka-desktop",
9
+ "keychainService": "ai.ekka.desktop"
10
+ },
11
+ "engine": {
12
+ "url": ""
13
+ }
14
+ }
@@ -10,7 +10,7 @@ edition = "2021"
10
10
 
11
11
  [build-dependencies]
12
12
  tauri-build = { version = "2", features = [] }
13
- dotenvy = "0.15"
13
+ serde_json = "1"
14
14
 
15
15
  [dependencies]
16
16
  tauri = { version = "2", features = [] }
@@ -1,47 +1,79 @@
1
+ use std::fs;
2
+
1
3
  fn main() {
2
- // Rerun if env files change (check both src-tauri and project root)
3
- println!("cargo:rerun-if-changed=.env");
4
- println!("cargo:rerun-if-changed=.env.local");
5
- println!("cargo:rerun-if-changed=../.env");
6
- println!("cargo:rerun-if-changed=../.env.local");
7
-
8
- // Load env files: try src-tauri first, then project root
9
- // .env.local takes precedence over .env
10
- let _ = dotenvy::from_filename(".env.local");
11
- let _ = dotenvy::from_filename(".env");
12
- let _ = dotenvy::from_filename("../.env.local");
13
- let _ = dotenvy::from_filename("../.env");
14
-
15
- // EKKA_ENGINE_URL is required at build time
16
- let engine_url = std::env::var("EKKA_ENGINE_URL").unwrap_or_else(|_| {
4
+ // Rerun if config changes
5
+ println!("cargo:rerun-if-changed=../app.config.json");
6
+
7
+ // 1. Read app.config.json
8
+ let config_path = "../app.config.json";
9
+ let config_str = fs::read_to_string(config_path).unwrap_or_else(|_| {
17
10
  panic!(
18
11
  "\n\n\
19
12
  ╔══════════════════════════════════════════════════════════════════╗\n\
20
- ║ BUILD ERROR: EKKA_ENGINE_URL is not set ║\n\
13
+ ║ BUILD ERROR: app.config.json not found ║\n\
21
14
  ╠══════════════════════════════════════════════════════════════════╣\n\
22
- Add EKKA_ENGINE_URL to .env.local or .env before building: ║\n\
15
+ This file is required and should have been created by CDA. ║\n\
16
+ ║ Regenerate your app with: ║\n\
23
17
  ║ ║\n\
24
- echo 'EKKA_ENGINE_URL=https://api.ekka.ai' >> .env.local ║\n\
18
+ npx create-ekka-desktop-app@latest my-app ║\n\
25
19
  ║ ║\n\
26
20
  ╚══════════════════════════════════════════════════════════════════╝\n\n"
27
21
  )
28
22
  });
29
23
 
30
- if engine_url.trim().is_empty() {
24
+ let config: serde_json::Value = serde_json::from_str(&config_str).unwrap_or_else(|e| {
25
+ panic!("\n\nBUILD ERROR: Invalid app.config.json: {}\n\n", e)
26
+ });
27
+
28
+ // 2. Extract and validate required fields
29
+ let app = config.get("app").expect("app.config.json missing 'app' section");
30
+ let storage = config.get("storage").expect("app.config.json missing 'storage' section");
31
+ let engine = config.get("engine").expect("app.config.json missing 'engine' section");
32
+
33
+ let app_name = app.get("name").and_then(|v| v.as_str())
34
+ .expect("app.config.json: app.name is required");
35
+ let app_slug = app.get("slug").and_then(|v| v.as_str())
36
+ .expect("app.config.json: app.slug is required");
37
+ let app_identifier = app.get("identifier").and_then(|v| v.as_str())
38
+ .expect("app.config.json: app.identifier is required");
39
+
40
+ let home_folder = storage.get("homeFolderName").and_then(|v| v.as_str())
41
+ .expect("app.config.json: storage.homeFolderName is required");
42
+ let keychain_service = storage.get("keychainService").and_then(|v| v.as_str())
43
+ .expect("app.config.json: storage.keychainService is required");
44
+
45
+ let engine_url = engine.get("url").and_then(|v| v.as_str())
46
+ .expect("app.config.json: engine.url is required");
47
+
48
+ // 3. Validate non-empty
49
+ if app_slug.is_empty() {
50
+ panic!("\n\nBUILD ERROR: app.slug cannot be empty in app.config.json\n\n");
51
+ }
52
+ if home_folder.is_empty() {
53
+ panic!("\n\nBUILD ERROR: storage.homeFolderName cannot be empty in app.config.json\n\n");
54
+ }
55
+ if engine_url.is_empty() {
31
56
  panic!(
32
57
  "\n\n\
33
58
  ╔══════════════════════════════════════════════════════════════════╗\n\
34
- ║ BUILD ERROR: EKKA_ENGINE_URL is empty ║\n\
59
+ ║ BUILD ERROR: engine.url is empty in app.config.json ║\n\
35
60
  ╠══════════════════════════════════════════════════════════════════╣\n\
36
- ║ Set a valid URL in .env.local or .env: ║\n\
61
+ ║ Set your EKKA Engine URL in app.config.json: ║\n\
37
62
  ║ ║\n\
38
- EKKA_ENGINE_URL=https://api.ekka.ai ║\n\
63
+ \"engine\": {{ ║\n\
64
+ ║ \"url\": \"https://api.ekka.ai\" ║\n\
65
+ ║ }} ║\n\
39
66
  ║ ║\n\
40
67
  ╚══════════════════════════════════════════════════════════════════╝\n\n"
41
- )
68
+ );
42
69
  }
43
70
 
44
- // Bake EKKA_ENGINE_URL into the binary at compile time
71
+ // 4. Bake values into binary at compile time
72
+ println!("cargo:rustc-env=EKKA_APP_NAME={}", app_name);
73
+ println!("cargo:rustc-env=EKKA_APP_SLUG={}", app_slug);
74
+ println!("cargo:rustc-env=EKKA_APP_IDENTIFIER={}", app_identifier);
75
+ println!("cargo:rustc-env=EKKA_HOME_FOLDER={}", home_folder);
76
+ println!("cargo:rustc-env=EKKA_KEYCHAIN_SERVICE={}", keychain_service);
45
77
  println!("cargo:rustc-env=EKKA_ENGINE_URL={}", engine_url);
46
78
 
47
79
  // Continue with tauri build
@@ -2,19 +2,20 @@
2
2
  //!
3
3
  //! Handles initialization and resolution of the EKKA home directory.
4
4
 
5
+ use crate::config;
5
6
  use ekka_sdk_core::ekka_home_bootstrap::{BootstrapConfig, EpochSource, HomeBootstrap, HomeStrategy};
6
7
  use std::path::PathBuf;
7
8
 
8
- /// Standard bootstrap configuration for EKKA Desktop
9
+ /// Standard bootstrap configuration - all values from app.config.json (baked at build time)
9
10
  pub fn bootstrap_config() -> BootstrapConfig {
10
11
  BootstrapConfig {
11
- app_name: "ekka-desktop".to_string(),
12
- default_folder_name: ".ekka-desktop".to_string(),
12
+ app_name: config::app_name().to_string(),
13
+ default_folder_name: config::home_folder().to_string(),
13
14
  home_strategy: HomeStrategy::DataHome {
14
15
  env_var: "EKKA_DATA_HOME".to_string(),
15
16
  },
16
17
  marker_filename: ".ekka-marker.json".to_string(),
17
- keychain_service: "ai.ekka.desktop".to_string(),
18
+ keychain_service: config::keychain_service().to_string(),
18
19
  subdirs: vec!["vault".to_string(), "db".to_string(), "tmp".to_string()],
19
20
  epoch_source: EpochSource::EnvVar("EKKA_SECURITY_EPOCH".to_string()),
20
21
  storage_layout_version: "v1".to_string(),
@@ -3,6 +3,7 @@
3
3
  //! Entry points for TypeScript → Rust communication.
4
4
 
5
5
  use crate::bootstrap::initialize_home;
6
+ use crate::config;
6
7
  use crate::engine_process;
7
8
  use crate::grants::require_home_granted;
8
9
  use crate::handlers;
@@ -541,7 +542,7 @@ fn handle_runner_task_stats(state: &EngineState) -> EngineResponse {
541
542
  .header("X-EKKA-CORRELATION-ID", &request_id)
542
543
  .header("X-EKKA-MODULE", "engine.runner_tasks")
543
544
  .header("X-EKKA-ACTION", "stats")
544
- .header("X-EKKA-CLIENT", "ekka-desktop")
545
+ .header("X-EKKA-CLIENT", config::app_slug())
545
546
  .header("X-EKKA-CLIENT-VERSION", "0.2.0")
546
547
  .send();
547
548
 
@@ -902,7 +903,7 @@ fn build_security_headers(jwt: Option<&str>, module: &str, action: &str) -> Vec<
902
903
  ("X-EKKA-PROOF-TYPE".to_string(), if jwt.is_some() { "jwt" } else { "none" }.to_string()),
903
904
  ("X-EKKA-MODULE".to_string(), module.to_string()),
904
905
  ("X-EKKA-ACTION".to_string(), action.to_string()),
905
- ("X-EKKA-CLIENT".to_string(), "ekka-desktop".to_string()),
906
+ ("X-EKKA-CLIENT".to_string(), config::app_slug().to_string()),
906
907
  ("X-EKKA-CLIENT-VERSION".to_string(), "0.2.0".to_string()),
907
908
  ];
908
909
 
@@ -0,0 +1,35 @@
1
+ //! Compile-time app configuration
2
+ //!
3
+ //! All values are baked at build time from app.config.json.
4
+ //! ZERO runtime configuration or hardcoding.
5
+
6
+ #![allow(dead_code)] // Not all config values are used yet
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 Studio")
20
+ baked_config!(app_name, "EKKA_APP_NAME");
21
+
22
+ // App slug for machine use (e.g., "ekka-studio")
23
+ baked_config!(app_slug, "EKKA_APP_SLUG");
24
+
25
+ // App identifier for OS (e.g., "ai.ekka.studio")
26
+ baked_config!(app_identifier, "EKKA_APP_IDENTIFIER");
27
+
28
+ // Home folder name (e.g., ".ekka-studio")
29
+ baked_config!(home_folder, "EKKA_HOME_FOLDER");
30
+
31
+ // Keychain service identifier (e.g., "ai.ekka.studio")
32
+ baked_config!(keychain_service, "EKKA_KEYCHAIN_SERVICE");
33
+
34
+ // EKKA Engine URL (e.g., "https://api.ekka.ai")
35
+ baked_config!(engine_url, "EKKA_ENGINE_URL");
@@ -7,6 +7,7 @@
7
7
 
8
8
  mod bootstrap;
9
9
  mod commands;
10
+ mod config;
10
11
  mod device_secret;
11
12
  mod engine_process;
12
13
  mod grants;
@@ -17,6 +17,7 @@
17
17
 
18
18
  #![allow(dead_code)] // API types may not all be used yet
19
19
 
20
+ use crate::config;
20
21
  use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
21
22
  use chrono::{DateTime, Utc};
22
23
  use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
@@ -240,7 +241,8 @@ pub fn get_private_key_vault_path(node_id: &Uuid) -> PathBuf {
240
241
  fn derive_node_encryption_key(node_id: &Uuid, device_fingerprint: Option<&str>) -> KeyMaterial {
241
242
  // Use node_id + device fingerprint for key derivation
242
243
  // This binds the key to this specific node on this device
243
- let device_secret = device_fingerprint.unwrap_or("ekka-desktop-default-device");
244
+ let default_device = format!("{}-default-device", config::app_slug());
245
+ let device_secret = device_fingerprint.unwrap_or(&default_device);
244
246
 
245
247
  derive_key(
246
248
  device_secret,
@@ -386,7 +388,7 @@ pub fn register_node(
386
388
  "node_id": node_id.to_string(),
387
389
  "public_key_b64": public_key_b64,
388
390
  "default_workspace_id": default_workspace_id,
389
- "display_name": "ekka-desktop",
391
+ "display_name": config::app_name(),
390
392
  "node_type": "desktop"
391
393
  });
392
394
 
@@ -403,7 +405,7 @@ pub fn register_node(
403
405
  .header("X-EKKA-CORRELATION-ID", &request_id)
404
406
  .header("X-EKKA-MODULE", "desktop.node_auth")
405
407
  .header("X-EKKA-ACTION", "register")
406
- .header("X-EKKA-CLIENT", "ekka-desktop")
408
+ .header("X-EKKA-CLIENT", config::app_slug())
407
409
  .header("X-EKKA-CLIENT-VERSION", "0.2.0")
408
410
  .json(&body)
409
411
  .send()
@@ -459,7 +461,7 @@ pub fn get_challenge(engine_url: &str, node_id: &Uuid) -> Result<ChallengeRespon
459
461
  .header("X-EKKA-CORRELATION-ID", &request_id)
460
462
  .header("X-EKKA-MODULE", "desktop.node_auth")
461
463
  .header("X-EKKA-ACTION", "challenge")
462
- .header("X-EKKA-CLIENT", "ekka-desktop")
464
+ .header("X-EKKA-CLIENT", config::app_slug())
463
465
  .header("X-EKKA-CLIENT-VERSION", "0.2.0")
464
466
  .json(&serde_json::json!({ "node_id": node_id.to_string() }))
465
467
  .send()
@@ -503,7 +505,7 @@ pub fn create_session(
503
505
  .header("X-EKKA-CORRELATION-ID", &request_id)
504
506
  .header("X-EKKA-MODULE", "desktop.node_auth")
505
507
  .header("X-EKKA-ACTION", "session")
506
- .header("X-EKKA-CLIENT", "ekka-desktop")
508
+ .header("X-EKKA-CLIENT", config::app_slug())
507
509
  .header("X-EKKA-CLIENT-VERSION", "0.2.0")
508
510
  .json(&serde_json::json!({
509
511
  "node_id": node_id.to_string(),
@@ -798,7 +800,7 @@ impl NodeSessionRunnerConfig {
798
800
  ("X-REQUEST-ID", Uuid::new_v4().to_string()),
799
801
  ("X-EKKA-CORRELATION-ID", Uuid::new_v4().to_string()),
800
802
  ("X-EKKA-MODULE", "engine.runner_tasks".to_string()),
801
- ("X-EKKA-CLIENT", "ekka-desktop".to_string()),
803
+ ("X-EKKA-CLIENT", config::app_slug().to_string()),
802
804
  ("X-EKKA-CLIENT-VERSION", "0.2.0".to_string()),
803
805
  ("X-EKKA-NODE-ID", self.node_id.to_string()),
804
806
  ]
@@ -11,6 +11,7 @@
11
11
  //! - No OS keychain prompts
12
12
 
13
13
  use crate::bootstrap::{initialize_home, resolve_home_path};
14
+ use crate::config;
14
15
  use crate::node_vault_store::{
15
16
  delete_node_secret, has_node_secret, read_node_secret, write_node_secret,
16
17
  SECRET_ID_NODE_CREDENTIALS,
@@ -394,7 +395,7 @@ pub fn authenticate_node(engine_url: &str) -> Result<NodeAuthToken, CredentialsE
394
395
  .header("X-EKKA-CORRELATION-ID", &request_id)
395
396
  .header("X-EKKA-MODULE", "desktop.node_auth")
396
397
  .header("X-EKKA-ACTION", "authenticate")
397
- .header("X-EKKA-CLIENT", "ekka-desktop")
398
+ .header("X-EKKA-CLIENT", config::app_slug())
398
399
  .header("X-EKKA-CLIENT-VERSION", "0.2.0")
399
400
  .json(&body)
400
401
  .send()
@@ -17,6 +17,7 @@
17
17
 
18
18
  #![allow(dead_code)] // API types and fields may not all be used yet
19
19
 
20
+ use crate::config;
20
21
  use crate::node_auth::{
21
22
  refresh_node_session, NodeSession, NodeSessionHolder, NodeSessionRunnerConfig,
22
23
  };
@@ -236,7 +237,7 @@ impl NodeSessionRunner {
236
237
  ("X-REQUEST-ID", Uuid::new_v4().to_string()),
237
238
  ("X-EKKA-CORRELATION-ID", Uuid::new_v4().to_string()),
238
239
  ("X-EKKA-MODULE", "engine.runner_tasks".to_string()),
239
- ("X-EKKA-CLIENT", "ekka-desktop".to_string()),
240
+ ("X-EKKA-CLIENT", config::app_slug().to_string()),
240
241
  ("X-EKKA-CLIENT-VERSION", "0.2.0".to_string()),
241
242
  ("X-EKKA-NODE-ID", self.node_id.to_string()),
242
243
  ])
@@ -700,7 +701,7 @@ impl NodeSessionRunnerHeartbeat {
700
701
  .header("X-EKKA-CORRELATION-ID", Uuid::new_v4().to_string())
701
702
  .header("X-EKKA-MODULE", "engine.runner_tasks")
702
703
  .header("X-EKKA-ACTION", "heartbeat")
703
- .header("X-EKKA-CLIENT", "ekka-desktop")
704
+ .header("X-EKKA-CLIENT", config::app_slug())
704
705
  .header("X-EKKA-CLIENT-VERSION", "0.2.0")
705
706
  .header("X-EKKA-NODE-ID", self.node_id.to_string())
706
707
  .json(&serde_json::json!({ "runner_id": self.runner_id }))