create-ekka-desktop-app 0.2.2
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/README.md +137 -0
- package/bin/cli.js +72 -0
- package/package.json +23 -0
- package/template/branding/app.json +6 -0
- package/template/branding/icon.icns +0 -0
- package/template/eslint.config.js +98 -0
- package/template/index.html +29 -0
- package/template/package.json +40 -0
- package/template/src/app/App.tsx +24 -0
- package/template/src/demo/DemoApp.tsx +260 -0
- package/template/src/demo/components/Banner.tsx +82 -0
- package/template/src/demo/components/EmptyState.tsx +61 -0
- package/template/src/demo/components/InfoPopover.tsx +171 -0
- package/template/src/demo/components/InfoTooltip.tsx +76 -0
- package/template/src/demo/components/LearnMore.tsx +98 -0
- package/template/src/demo/components/NodeCredentialsOnboarding.tsx +219 -0
- package/template/src/demo/components/SetupWizard.tsx +48 -0
- package/template/src/demo/components/StatusBadge.tsx +83 -0
- package/template/src/demo/components/index.ts +10 -0
- package/template/src/demo/hooks/index.ts +6 -0
- package/template/src/demo/hooks/useAuditEvents.ts +30 -0
- package/template/src/demo/layout/Shell.tsx +110 -0
- package/template/src/demo/layout/Sidebar.tsx +192 -0
- package/template/src/demo/pages/AuditLogPage.tsx +235 -0
- package/template/src/demo/pages/DocGenPage.tsx +874 -0
- package/template/src/demo/pages/HomeSetupPage.tsx +182 -0
- package/template/src/demo/pages/LoginPage.tsx +192 -0
- package/template/src/demo/pages/PathPermissionsPage.tsx +873 -0
- package/template/src/demo/pages/RunnerPage.tsx +445 -0
- package/template/src/demo/pages/SystemPage.tsx +557 -0
- package/template/src/demo/pages/VaultPage.tsx +805 -0
- package/template/src/ekka/__tests__/demo-backend.test.ts +187 -0
- package/template/src/ekka/audit/index.ts +7 -0
- package/template/src/ekka/audit/store.ts +68 -0
- package/template/src/ekka/audit/types.ts +22 -0
- package/template/src/ekka/auth/client.ts +212 -0
- package/template/src/ekka/auth/index.ts +30 -0
- package/template/src/ekka/auth/storage.ts +114 -0
- package/template/src/ekka/auth/types.ts +67 -0
- package/template/src/ekka/backend/demo.ts +151 -0
- package/template/src/ekka/backend/interface.ts +36 -0
- package/template/src/ekka/config.ts +48 -0
- package/template/src/ekka/constants.ts +143 -0
- package/template/src/ekka/errors.ts +54 -0
- package/template/src/ekka/index.ts +516 -0
- package/template/src/ekka/internal/backend.ts +156 -0
- package/template/src/ekka/internal/index.ts +7 -0
- package/template/src/ekka/ops/auth.ts +29 -0
- package/template/src/ekka/ops/debug.ts +68 -0
- package/template/src/ekka/ops/home.ts +101 -0
- package/template/src/ekka/ops/index.ts +16 -0
- package/template/src/ekka/ops/nodeCredentials.ts +131 -0
- package/template/src/ekka/ops/nodeSession.ts +145 -0
- package/template/src/ekka/ops/paths.ts +183 -0
- package/template/src/ekka/ops/runner.ts +86 -0
- package/template/src/ekka/ops/runtime.ts +31 -0
- package/template/src/ekka/ops/setup.ts +47 -0
- package/template/src/ekka/ops/vault.ts +459 -0
- package/template/src/ekka/ops/workflowRuns.ts +116 -0
- package/template/src/ekka/types.ts +82 -0
- package/template/src/ekka/utils/idempotency.ts +14 -0
- package/template/src/ekka/utils/index.ts +7 -0
- package/template/src/ekka/utils/time.ts +77 -0
- package/template/src/main.tsx +12 -0
- package/template/src/vite-env.d.ts +12 -0
- package/template/src-tauri/Cargo.toml +41 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +11 -0
- package/template/src-tauri/icons/icon.icns +0 -0
- package/template/src-tauri/icons/icon.png +0 -0
- package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
- package/template/src-tauri/src/bootstrap.rs +37 -0
- package/template/src-tauri/src/commands.rs +1215 -0
- package/template/src-tauri/src/device_secret.rs +111 -0
- package/template/src-tauri/src/engine_process.rs +538 -0
- package/template/src-tauri/src/grants.rs +129 -0
- package/template/src-tauri/src/handlers/home.rs +65 -0
- package/template/src-tauri/src/handlers/mod.rs +7 -0
- package/template/src-tauri/src/handlers/paths.rs +128 -0
- package/template/src-tauri/src/handlers/vault.rs +680 -0
- package/template/src-tauri/src/main.rs +243 -0
- package/template/src-tauri/src/node_auth.rs +858 -0
- package/template/src-tauri/src/node_credentials.rs +541 -0
- package/template/src-tauri/src/node_runner.rs +882 -0
- package/template/src-tauri/src/node_vault_crypto.rs +113 -0
- package/template/src-tauri/src/node_vault_store.rs +267 -0
- package/template/src-tauri/src/ops/auth.rs +50 -0
- package/template/src-tauri/src/ops/home.rs +251 -0
- package/template/src-tauri/src/ops/mod.rs +7 -0
- package/template/src-tauri/src/ops/runtime.rs +21 -0
- package/template/src-tauri/src/state.rs +639 -0
- package/template/src-tauri/src/types.rs +84 -0
- package/template/src-tauri/tauri.conf.json +41 -0
- package/template/tsconfig.json +26 -0
- package/template/tsconfig.tsbuildinfo +1 -0
- package/template/vite.config.ts +34 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//! Grant validation
|
|
2
|
+
//!
|
|
3
|
+
//! Handles verification of HOME grants and home status checks.
|
|
4
|
+
|
|
5
|
+
use crate::bootstrap::resolve_home_path;
|
|
6
|
+
use crate::state::{AuthContext, EngineState, HomeState};
|
|
7
|
+
use crate::types::EngineResponse;
|
|
8
|
+
use ekka_sdk_core::ekka_path_guard::GrantStore;
|
|
9
|
+
use std::path::PathBuf;
|
|
10
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
11
|
+
|
|
12
|
+
/// Check if a valid HOME grant exists for the given auth context
|
|
13
|
+
pub fn check_home_grant(home_path: &PathBuf, auth: &AuthContext) -> Result<bool, String> {
|
|
14
|
+
let grants_path = home_path.join("grants.json");
|
|
15
|
+
|
|
16
|
+
// No grants file = no grant
|
|
17
|
+
if !grants_path.exists() {
|
|
18
|
+
return Ok(false);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Load engine verify key
|
|
22
|
+
let key_b64 = match std::env::var("ENGINE_GRANT_VERIFY_KEY_B64") {
|
|
23
|
+
Ok(k) => k,
|
|
24
|
+
Err(_) => return Err("ENGINE_GRANT_VERIFY_KEY_B64 not set".to_string()),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Load and verify grants
|
|
28
|
+
let store = GrantStore::new(grants_path, &key_b64).map_err(|e| e.to_string())?;
|
|
29
|
+
let grants = store.grants();
|
|
30
|
+
|
|
31
|
+
// Check for valid HOME grant matching auth context
|
|
32
|
+
let now = SystemTime::now()
|
|
33
|
+
.duration_since(UNIX_EPOCH)
|
|
34
|
+
.unwrap_or_default()
|
|
35
|
+
.as_secs() as i64;
|
|
36
|
+
|
|
37
|
+
for grant in grants {
|
|
38
|
+
// Check if this is a HOME grant (covers home_path)
|
|
39
|
+
let home_str = home_path.to_string_lossy();
|
|
40
|
+
if !home_str.starts_with(grant.path_prefix()) && grant.path_prefix() != home_str {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check tenant_id matches
|
|
45
|
+
if grant.tenant_id() != auth.tenant_id {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check sub matches
|
|
50
|
+
if grant.subject() != auth.sub {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check not expired
|
|
55
|
+
if grant.expires_at() < now {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Valid HOME grant found
|
|
60
|
+
return Ok(true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Ok(false)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Get current home status including state, path, and grant presence
|
|
67
|
+
pub fn get_home_status(state: &EngineState) -> (HomeState, PathBuf, bool, Option<String>) {
|
|
68
|
+
// Resolve home path
|
|
69
|
+
let home_path = match resolve_home_path() {
|
|
70
|
+
Ok(p) => p,
|
|
71
|
+
Err(e) => {
|
|
72
|
+
return (
|
|
73
|
+
HomeState::BootstrapPreLogin,
|
|
74
|
+
PathBuf::new(),
|
|
75
|
+
false,
|
|
76
|
+
Some(format!("Failed to resolve home path: {}", e)),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Store home path
|
|
82
|
+
if let Ok(mut hp) = state.home_path.lock() {
|
|
83
|
+
*hp = Some(home_path.clone());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check auth
|
|
87
|
+
let auth = match state.auth.lock() {
|
|
88
|
+
Ok(guard) => guard.clone(),
|
|
89
|
+
Err(_) => {
|
|
90
|
+
return (
|
|
91
|
+
HomeState::BootstrapPreLogin,
|
|
92
|
+
home_path,
|
|
93
|
+
false,
|
|
94
|
+
Some("Lock error".to_string()),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
let auth = match auth {
|
|
100
|
+
Some(a) => a,
|
|
101
|
+
None => return (HomeState::BootstrapPreLogin, home_path, false, None),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Check for valid HOME grant
|
|
105
|
+
match check_home_grant(&home_path, &auth) {
|
|
106
|
+
Ok(true) => (HomeState::HomeGranted, home_path, true, None),
|
|
107
|
+
Ok(false) => (
|
|
108
|
+
HomeState::AuthenticatedNoHomeGrant,
|
|
109
|
+
home_path,
|
|
110
|
+
false,
|
|
111
|
+
Some("No valid HOME grant found".to_string()),
|
|
112
|
+
),
|
|
113
|
+
Err(e) => (HomeState::AuthenticatedNoHomeGrant, home_path, false, Some(e)),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Guard function: returns error response if HOME grant is not present
|
|
118
|
+
pub fn require_home_granted(state: &EngineState) -> Result<(), EngineResponse> {
|
|
119
|
+
let (home_state, _, _, reason) = get_home_status(state);
|
|
120
|
+
|
|
121
|
+
if home_state != HomeState::HomeGranted {
|
|
122
|
+
return Err(EngineResponse::err(
|
|
123
|
+
"HOME_GRANT_REQUIRED",
|
|
124
|
+
&reason.unwrap_or_else(|| "HOME grant required before this operation".to_string()),
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Ok(())
|
|
129
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//! Home operation handlers
|
|
2
|
+
//!
|
|
3
|
+
//! Thin wrappers that call SDK operations.
|
|
4
|
+
|
|
5
|
+
use crate::state::{EngineHttpGrantIssuer, EngineState};
|
|
6
|
+
use crate::types::EngineResponse;
|
|
7
|
+
use ekka_sdk_core::ekka_ops::home;
|
|
8
|
+
use serde_json::json;
|
|
9
|
+
|
|
10
|
+
/// Handle home.status operation
|
|
11
|
+
pub fn handle_status(state: &EngineState) -> EngineResponse {
|
|
12
|
+
let ctx = match state.to_runtime_context() {
|
|
13
|
+
Some(c) => c,
|
|
14
|
+
None => {
|
|
15
|
+
// No context yet - return pre-login state
|
|
16
|
+
let home_path = state
|
|
17
|
+
.home_path
|
|
18
|
+
.lock()
|
|
19
|
+
.ok()
|
|
20
|
+
.and_then(|p| p.clone())
|
|
21
|
+
.map(|p| p.to_string_lossy().to_string())
|
|
22
|
+
.unwrap_or_default();
|
|
23
|
+
|
|
24
|
+
return EngineResponse::ok(json!({
|
|
25
|
+
"state": "BOOTSTRAP_PRE_LOGIN",
|
|
26
|
+
"homePath": home_path,
|
|
27
|
+
"grantPresent": false,
|
|
28
|
+
"reason": null,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let status = home::status(&ctx);
|
|
34
|
+
|
|
35
|
+
EngineResponse::ok(json!({
|
|
36
|
+
"state": status.state,
|
|
37
|
+
"homePath": status.home_path,
|
|
38
|
+
"grantPresent": status.grant_present,
|
|
39
|
+
"reason": status.reason,
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Handle home.grant operation
|
|
44
|
+
pub fn handle_grant(state: &EngineState) -> EngineResponse {
|
|
45
|
+
let ctx = match state.to_runtime_context() {
|
|
46
|
+
Some(c) => c,
|
|
47
|
+
None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Check auth
|
|
51
|
+
if ctx.auth.is_none() {
|
|
52
|
+
return EngineResponse::err("NOT_AUTHENTICATED", "Must call auth.set before home.grant");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let issuer = EngineHttpGrantIssuer::new();
|
|
56
|
+
|
|
57
|
+
match home::grant(&ctx, &issuer) {
|
|
58
|
+
Ok(result) => EngineResponse::ok(json!({
|
|
59
|
+
"success": result.success,
|
|
60
|
+
"grant_id": result.grant_id,
|
|
61
|
+
"expires_at": result.expires_at,
|
|
62
|
+
})),
|
|
63
|
+
Err(e) => EngineResponse::err(e.code, &e.message),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//! Path operation handlers
|
|
2
|
+
//!
|
|
3
|
+
//! Thin wrappers that call SDK operations.
|
|
4
|
+
|
|
5
|
+
use crate::state::{EngineHttpGrantIssuer, EngineState};
|
|
6
|
+
use crate::types::EngineResponse;
|
|
7
|
+
use ekka_sdk_core::ekka_ops::{paths, PathAccess, PathType};
|
|
8
|
+
use serde_json::{json, Value};
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
|
|
11
|
+
/// Handle paths.check operation
|
|
12
|
+
pub fn handle_check(payload: &Value, state: &EngineState) -> EngineResponse {
|
|
13
|
+
let ctx = match state.to_runtime_context() {
|
|
14
|
+
Some(c) => c,
|
|
15
|
+
None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let path = match payload.get("path").and_then(|v| v.as_str()) {
|
|
19
|
+
Some(p) => p,
|
|
20
|
+
None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let operation = payload
|
|
24
|
+
.get("operation")
|
|
25
|
+
.and_then(|v| v.as_str())
|
|
26
|
+
.unwrap_or("read");
|
|
27
|
+
|
|
28
|
+
let result = paths::check_detailed(&ctx, Path::new(path), operation);
|
|
29
|
+
|
|
30
|
+
EngineResponse::ok(json!({
|
|
31
|
+
"allowed": result.allowed,
|
|
32
|
+
"reason": result.reason,
|
|
33
|
+
"pathType": result.path_type,
|
|
34
|
+
"access": result.access,
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Handle paths.list operation
|
|
39
|
+
pub fn handle_list(payload: &Value, state: &EngineState) -> EngineResponse {
|
|
40
|
+
let ctx = match state.to_runtime_context() {
|
|
41
|
+
Some(c) => c,
|
|
42
|
+
None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let path_type: Option<PathType> = payload
|
|
46
|
+
.get("pathType")
|
|
47
|
+
.and_then(|v| v.as_str())
|
|
48
|
+
.and_then(|s| serde_json::from_str(&format!("\"{}\"", s)).ok());
|
|
49
|
+
|
|
50
|
+
match paths::list(&ctx, path_type) {
|
|
51
|
+
Ok(paths_list) => EngineResponse::ok(json!({ "paths": paths_list })),
|
|
52
|
+
Err(e) => EngineResponse::err(e.code, &e.message),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Handle paths.get operation
|
|
57
|
+
pub fn handle_get(payload: &Value, state: &EngineState) -> EngineResponse {
|
|
58
|
+
let ctx = match state.to_runtime_context() {
|
|
59
|
+
Some(c) => c,
|
|
60
|
+
None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let path = match payload.get("path").and_then(|v| v.as_str()) {
|
|
64
|
+
Some(p) => p,
|
|
65
|
+
None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
match paths::get(&ctx, Path::new(path)) {
|
|
69
|
+
Ok(Some(info)) => EngineResponse::ok(json!(info)),
|
|
70
|
+
Ok(None) => EngineResponse::ok(Value::Null),
|
|
71
|
+
Err(e) => EngineResponse::err(e.code, &e.message),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Handle paths.request operation
|
|
76
|
+
pub fn handle_request(payload: &Value, state: &EngineState) -> EngineResponse {
|
|
77
|
+
let ctx = match state.to_runtime_context() {
|
|
78
|
+
Some(c) => c,
|
|
79
|
+
None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Check auth
|
|
83
|
+
if ctx.auth.is_none() {
|
|
84
|
+
return EngineResponse::err("NOT_AUTHENTICATED", "Must login before requesting path access");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let path = match payload.get("path").and_then(|v| v.as_str()) {
|
|
88
|
+
Some(p) => p,
|
|
89
|
+
None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let path_type: PathType = payload
|
|
93
|
+
.get("pathType")
|
|
94
|
+
.and_then(|v| v.as_str())
|
|
95
|
+
.and_then(|s| serde_json::from_str(&format!("\"{}\"", s)).ok())
|
|
96
|
+
.unwrap_or(PathType::General);
|
|
97
|
+
|
|
98
|
+
let access: PathAccess = payload
|
|
99
|
+
.get("access")
|
|
100
|
+
.and_then(|v| v.as_str())
|
|
101
|
+
.and_then(|s| serde_json::from_str(&format!("\"{}\"", s)).ok())
|
|
102
|
+
.unwrap_or(PathAccess::ReadOnly);
|
|
103
|
+
|
|
104
|
+
let issuer = EngineHttpGrantIssuer::new();
|
|
105
|
+
|
|
106
|
+
match paths::request(&ctx, &issuer, Path::new(path), path_type, access) {
|
|
107
|
+
Ok(result) => EngineResponse::ok(json!(result)),
|
|
108
|
+
Err(e) => EngineResponse::err(e.code, &e.message),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Handle paths.remove operation
|
|
113
|
+
pub fn handle_remove(payload: &Value, state: &EngineState) -> EngineResponse {
|
|
114
|
+
let ctx = match state.to_runtime_context() {
|
|
115
|
+
Some(c) => c,
|
|
116
|
+
None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let path = match payload.get("path").and_then(|v| v.as_str()) {
|
|
120
|
+
Some(p) => p,
|
|
121
|
+
None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
match paths::remove(&ctx, Path::new(path)) {
|
|
125
|
+
Ok(removed) => EngineResponse::ok(json!({ "removed": removed })),
|
|
126
|
+
Err(e) => EngineResponse::err(e.code, &e.message),
|
|
127
|
+
}
|
|
128
|
+
}
|