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.
- package/bin/cli.js +1 -5
- package/package.json +1 -1
- package/template/src/demo/DemoApp.tsx +0 -44
- package/template/src/demo/layout/Sidebar.tsx +2 -13
- package/template/src/demo/pages/LoginPage.tsx +1 -2
- package/template/src/demo/pages/SystemPage.tsx +1 -1
- package/template/src/ekka/backend/demo.ts +1 -1
- package/template/src/ekka/constants.ts +0 -4
- package/template/src/ekka/index.ts +0 -2
- package/template/src/ekka/internal/backend.ts +8 -8
- package/template/src/ekka/ops/index.ts +1 -2
- package/template/src/ekka/utils/index.ts +0 -1
- package/template/src-tauri/Cargo.toml +2 -0
- package/template/src-tauri/crates/ekka-desktop-core/Cargo.toml +30 -0
- package/template/src-tauri/crates/ekka-desktop-core/build.rs +42 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/bootstrap.rs +39 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/config.rs +32 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/device_secret.rs +74 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/main.rs +1225 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/node_credentials.rs +413 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/node_vault_crypto.rs +57 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/node_vault_store.rs +198 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/security_epoch.rs +80 -0
- package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
- package/template/src-tauri/src/commands.rs +137 -958
- package/template/src-tauri/src/config.rs +4 -44
- package/template/src-tauri/src/core_process.rs +335 -0
- package/template/src-tauri/src/engine_process.rs +103 -0
- package/template/src-tauri/src/handlers/home.rs +1 -32
- package/template/src-tauri/src/main.rs +240 -153
- package/template/src-tauri/src/node_auth.rs +2 -748
- package/template/src-tauri/src/node_credentials.rs +2 -201
- package/template/src-tauri/src/node_runner.rs +2 -55
- package/template/src-tauri/src/node_vault_crypto.rs +0 -33
- package/template/src-tauri/src/node_vault_store.rs +1 -150
- package/template/src-tauri/src/ops/mod.rs +0 -2
- package/template/src-tauri/src/state.rs +7 -63
- package/template/src-tauri/src/types.rs +1 -23
- package/template/src-tauri/src/updater.rs +215 -0
- package/template/src-tauri/tauri.conf.json +9 -0
- package/template/src/demo/pages/DocGenPage.tsx +0 -731
- package/template/src/ekka/config.ts +0 -48
- package/template/src/ekka/ops/debug.ts +0 -68
- package/template/src/ekka/ops/executionRuns.ts +0 -147
- package/template/src/ekka/ops/workflowRuns.ts +0 -119
- package/template/src/ekka/utils/idempotency.ts +0 -14
- package/template/src-tauri/src/ops/home.rs +0 -251
- package/template/src-tauri/src/ops/runtime.rs +0 -21
- 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
|
@@ -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' | '
|
|
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}>
|
|
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}>
|
|
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 === '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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 : '
|
|
111
|
-
console.error(`[ts.op.dispatch] op=${req.op} backend=
|
|
112
|
-
return err('
|
|
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
|
|
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';
|
|
@@ -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
|
+
}
|