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.
Files changed (96) hide show
  1. package/README.md +137 -0
  2. package/bin/cli.js +72 -0
  3. package/package.json +23 -0
  4. package/template/branding/app.json +6 -0
  5. package/template/branding/icon.icns +0 -0
  6. package/template/eslint.config.js +98 -0
  7. package/template/index.html +29 -0
  8. package/template/package.json +40 -0
  9. package/template/src/app/App.tsx +24 -0
  10. package/template/src/demo/DemoApp.tsx +260 -0
  11. package/template/src/demo/components/Banner.tsx +82 -0
  12. package/template/src/demo/components/EmptyState.tsx +61 -0
  13. package/template/src/demo/components/InfoPopover.tsx +171 -0
  14. package/template/src/demo/components/InfoTooltip.tsx +76 -0
  15. package/template/src/demo/components/LearnMore.tsx +98 -0
  16. package/template/src/demo/components/NodeCredentialsOnboarding.tsx +219 -0
  17. package/template/src/demo/components/SetupWizard.tsx +48 -0
  18. package/template/src/demo/components/StatusBadge.tsx +83 -0
  19. package/template/src/demo/components/index.ts +10 -0
  20. package/template/src/demo/hooks/index.ts +6 -0
  21. package/template/src/demo/hooks/useAuditEvents.ts +30 -0
  22. package/template/src/demo/layout/Shell.tsx +110 -0
  23. package/template/src/demo/layout/Sidebar.tsx +192 -0
  24. package/template/src/demo/pages/AuditLogPage.tsx +235 -0
  25. package/template/src/demo/pages/DocGenPage.tsx +874 -0
  26. package/template/src/demo/pages/HomeSetupPage.tsx +182 -0
  27. package/template/src/demo/pages/LoginPage.tsx +192 -0
  28. package/template/src/demo/pages/PathPermissionsPage.tsx +873 -0
  29. package/template/src/demo/pages/RunnerPage.tsx +445 -0
  30. package/template/src/demo/pages/SystemPage.tsx +557 -0
  31. package/template/src/demo/pages/VaultPage.tsx +805 -0
  32. package/template/src/ekka/__tests__/demo-backend.test.ts +187 -0
  33. package/template/src/ekka/audit/index.ts +7 -0
  34. package/template/src/ekka/audit/store.ts +68 -0
  35. package/template/src/ekka/audit/types.ts +22 -0
  36. package/template/src/ekka/auth/client.ts +212 -0
  37. package/template/src/ekka/auth/index.ts +30 -0
  38. package/template/src/ekka/auth/storage.ts +114 -0
  39. package/template/src/ekka/auth/types.ts +67 -0
  40. package/template/src/ekka/backend/demo.ts +151 -0
  41. package/template/src/ekka/backend/interface.ts +36 -0
  42. package/template/src/ekka/config.ts +48 -0
  43. package/template/src/ekka/constants.ts +143 -0
  44. package/template/src/ekka/errors.ts +54 -0
  45. package/template/src/ekka/index.ts +516 -0
  46. package/template/src/ekka/internal/backend.ts +156 -0
  47. package/template/src/ekka/internal/index.ts +7 -0
  48. package/template/src/ekka/ops/auth.ts +29 -0
  49. package/template/src/ekka/ops/debug.ts +68 -0
  50. package/template/src/ekka/ops/home.ts +101 -0
  51. package/template/src/ekka/ops/index.ts +16 -0
  52. package/template/src/ekka/ops/nodeCredentials.ts +131 -0
  53. package/template/src/ekka/ops/nodeSession.ts +145 -0
  54. package/template/src/ekka/ops/paths.ts +183 -0
  55. package/template/src/ekka/ops/runner.ts +86 -0
  56. package/template/src/ekka/ops/runtime.ts +31 -0
  57. package/template/src/ekka/ops/setup.ts +47 -0
  58. package/template/src/ekka/ops/vault.ts +459 -0
  59. package/template/src/ekka/ops/workflowRuns.ts +116 -0
  60. package/template/src/ekka/types.ts +82 -0
  61. package/template/src/ekka/utils/idempotency.ts +14 -0
  62. package/template/src/ekka/utils/index.ts +7 -0
  63. package/template/src/ekka/utils/time.ts +77 -0
  64. package/template/src/main.tsx +12 -0
  65. package/template/src/vite-env.d.ts +12 -0
  66. package/template/src-tauri/Cargo.toml +41 -0
  67. package/template/src-tauri/build.rs +3 -0
  68. package/template/src-tauri/capabilities/default.json +11 -0
  69. package/template/src-tauri/icons/icon.icns +0 -0
  70. package/template/src-tauri/icons/icon.png +0 -0
  71. package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
  72. package/template/src-tauri/src/bootstrap.rs +37 -0
  73. package/template/src-tauri/src/commands.rs +1215 -0
  74. package/template/src-tauri/src/device_secret.rs +111 -0
  75. package/template/src-tauri/src/engine_process.rs +538 -0
  76. package/template/src-tauri/src/grants.rs +129 -0
  77. package/template/src-tauri/src/handlers/home.rs +65 -0
  78. package/template/src-tauri/src/handlers/mod.rs +7 -0
  79. package/template/src-tauri/src/handlers/paths.rs +128 -0
  80. package/template/src-tauri/src/handlers/vault.rs +680 -0
  81. package/template/src-tauri/src/main.rs +243 -0
  82. package/template/src-tauri/src/node_auth.rs +858 -0
  83. package/template/src-tauri/src/node_credentials.rs +541 -0
  84. package/template/src-tauri/src/node_runner.rs +882 -0
  85. package/template/src-tauri/src/node_vault_crypto.rs +113 -0
  86. package/template/src-tauri/src/node_vault_store.rs +267 -0
  87. package/template/src-tauri/src/ops/auth.rs +50 -0
  88. package/template/src-tauri/src/ops/home.rs +251 -0
  89. package/template/src-tauri/src/ops/mod.rs +7 -0
  90. package/template/src-tauri/src/ops/runtime.rs +21 -0
  91. package/template/src-tauri/src/state.rs +639 -0
  92. package/template/src-tauri/src/types.rs +84 -0
  93. package/template/src-tauri/tauri.conf.json +41 -0
  94. package/template/tsconfig.json +26 -0
  95. package/template/tsconfig.tsbuildinfo +1 -0
  96. package/template/vite.config.ts +34 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * EKKA Demo Backend
3
+ *
4
+ * In-memory implementation for development/testing.
5
+ * Only implements ops that have real Rust implementations.
6
+ */
7
+
8
+ import type { EngineRequest, EngineResponse } from '../types';
9
+ import type { Backend } from './interface';
10
+ import { OPS, ERROR_CODES } from '../constants';
11
+ import { ok, err } from '../types';
12
+ import branding from '../../../branding/app.json';
13
+
14
+ // Derive display-only home path from branding (no OS detection, works in browser)
15
+ function slugify(name: string): string {
16
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
17
+ }
18
+ const DEMO_HOME_PATH = `~/.local/share/${slugify(branding.name) || 'ekka-desktop'}`;
19
+
20
+ /**
21
+ * Demo backend using in-memory storage.
22
+ * Used when Tauri engine is not available.
23
+ */
24
+ export class DemoBackend implements Backend {
25
+ private connected = false;
26
+ private authContext: { tenantId: string; sub: string; jwt: string } | null = null;
27
+ private homeGranted = false;
28
+
29
+ async connect(): Promise<void> {
30
+ await Promise.resolve();
31
+ this.connected = true;
32
+ }
33
+
34
+ disconnect(): void {
35
+ this.connected = false;
36
+ this.authContext = null;
37
+ this.homeGranted = false;
38
+ }
39
+
40
+ isConnected(): boolean {
41
+ return this.connected;
42
+ }
43
+
44
+ async request(req: EngineRequest): Promise<EngineResponse> {
45
+ await Promise.resolve();
46
+ return this.handle(req);
47
+ }
48
+
49
+ private handle(req: EngineRequest): EngineResponse {
50
+ // Check connection for non-runtime ops
51
+ if (req.op !== OPS.RUNTIME_INFO && !this.connected) {
52
+ return err(ERROR_CODES.NOT_CONNECTED, 'Not connected. Call ekka.connect() first.');
53
+ }
54
+
55
+ switch (req.op) {
56
+ // -----------------------------------------------------------------------
57
+ // Runtime
58
+ // -----------------------------------------------------------------------
59
+ case OPS.RUNTIME_INFO: {
60
+ return ok({
61
+ runtime: 'demo',
62
+ engine_present: false,
63
+ mode: 'demo',
64
+ homeState: this.getHomeState(),
65
+ homePath: DEMO_HOME_PATH,
66
+ });
67
+ }
68
+
69
+ // -----------------------------------------------------------------------
70
+ // Auth
71
+ // -----------------------------------------------------------------------
72
+ case OPS.AUTH_SET: {
73
+ const payload = req.payload as { tenantId?: string; sub?: string; jwt?: string };
74
+
75
+ if (!payload?.tenantId || !payload?.sub || !payload?.jwt) {
76
+ return err(ERROR_CODES.INVALID_PAYLOAD, 'Missing tenantId, sub, or jwt');
77
+ }
78
+
79
+ this.authContext = {
80
+ tenantId: payload.tenantId,
81
+ sub: payload.sub,
82
+ jwt: payload.jwt,
83
+ };
84
+
85
+ return ok({ ok: true });
86
+ }
87
+
88
+ // -----------------------------------------------------------------------
89
+ // Home
90
+ // -----------------------------------------------------------------------
91
+ case OPS.HOME_STATUS: {
92
+ return ok({
93
+ state: this.getHomeState(),
94
+ homePath: DEMO_HOME_PATH,
95
+ grantPresent: this.homeGranted,
96
+ reason: this.homeGranted ? null : 'Demo mode - call home.grant to simulate',
97
+ });
98
+ }
99
+
100
+ case OPS.HOME_GRANT: {
101
+ if (!this.authContext) {
102
+ return err(ERROR_CODES.NOT_AUTHENTICATED, 'Must call auth.set before home.grant');
103
+ }
104
+
105
+ // Simulate grant issuance
106
+ this.homeGranted = true;
107
+
108
+ return ok({
109
+ success: true,
110
+ grant_id: 'demo-grant-' + Date.now(),
111
+ expires_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
112
+ });
113
+ }
114
+
115
+ // -----------------------------------------------------------------------
116
+ // Setup (pre-login) - only checks node credentials
117
+ // Home folder grant is post-login
118
+ // -----------------------------------------------------------------------
119
+ case OPS.SETUP_STATUS: {
120
+ // In demo mode, node credentials are never configured
121
+ return ok({
122
+ nodeIdentity: 'not_configured',
123
+ setupComplete: false,
124
+ });
125
+ }
126
+
127
+ // -----------------------------------------------------------------------
128
+ // Unknown
129
+ // -----------------------------------------------------------------------
130
+ default:
131
+ return err(ERROR_CODES.INVALID_OP, `Unknown operation: ${req.op}`);
132
+ }
133
+ }
134
+
135
+ private getHomeState(): string {
136
+ if (this.homeGranted) return 'HOME_GRANTED';
137
+ if (this.authContext) return 'AUTHENTICATED_NO_HOME_GRANT';
138
+ return 'BOOTSTRAP_PRE_LOGIN';
139
+ }
140
+
141
+ /**
142
+ * Reset all state (for testing).
143
+ */
144
+ resetAll(): void {
145
+ this.connected = false;
146
+ this.authContext = null;
147
+ this.homeGranted = false;
148
+ }
149
+ }
150
+
151
+ export const demoBackend = new DemoBackend();
@@ -0,0 +1,36 @@
1
+ /**
2
+ * EKKA Backend Interface
3
+ *
4
+ * Defines the contract for all backend implementations.
5
+ * Both DemoBackend and EngineBackend implement this interface.
6
+ */
7
+
8
+ import type { EngineRequest, EngineResponse } from '../types';
9
+
10
+ /**
11
+ * Backend interface for EKKA operations.
12
+ * All requests flow through a backend implementing this interface.
13
+ */
14
+ export interface Backend {
15
+ /**
16
+ * Connect to the backend.
17
+ * Must be called before any db/queue operations.
18
+ */
19
+ connect(): Promise<void>;
20
+
21
+ /**
22
+ * Disconnect from the backend.
23
+ */
24
+ disconnect(): void;
25
+
26
+ /**
27
+ * Check if connected to the backend.
28
+ */
29
+ isConnected(): boolean;
30
+
31
+ /**
32
+ * Send a request to the backend and receive a response.
33
+ * Single RPC method for all operations.
34
+ */
35
+ request(req: EngineRequest): Promise<EngineResponse>;
36
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * EKKA Configuration
3
+ *
4
+ * API configuration and client identity constants.
5
+ */
6
+
7
+ // =============================================================================
8
+ // API CONFIGURATION
9
+ // =============================================================================
10
+
11
+ const getApiUrl = (): string => {
12
+ if (import.meta.env.PROD) return 'https://api.ekka.ai';
13
+ if (import.meta.env.VITE_EKKA_API_URL) return import.meta.env.VITE_EKKA_API_URL;
14
+ return 'https://api.ekka.ai';
15
+ };
16
+
17
+ /** API base URL */
18
+ export const API_BASE_URL = getApiUrl();
19
+
20
+ // =============================================================================
21
+ // ENGINE CONFIGURATION
22
+ // =============================================================================
23
+
24
+ const getEngineUrl = (): string => {
25
+ if (import.meta.env.VITE_EKKA_ENGINE_URL) return import.meta.env.VITE_EKKA_ENGINE_URL;
26
+ return 'http://localhost:3200'; // Default local engine port
27
+ };
28
+
29
+ /** Engine base URL for workflow runs */
30
+ export const ENGINE_BASE_URL = getEngineUrl();
31
+
32
+ // =============================================================================
33
+ // CLIENT IDENTITY (Security Envelope)
34
+ // =============================================================================
35
+
36
+ export const CLIENT_TYPE = 'desktop';
37
+ export const CLIENT_VERSION = '0.2.0';
38
+
39
+ // =============================================================================
40
+ // AUTH ENDPOINTS
41
+ // =============================================================================
42
+
43
+ export const AUTH_ENDPOINTS = {
44
+ login: '/auth/login',
45
+ refresh: '/auth/refresh',
46
+ logout: '/auth/logout',
47
+ me: '/auth/me',
48
+ } as const;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * EKKA Constants
3
+ *
4
+ * Operation names and error codes.
5
+ * Only includes IMPLEMENTED operations.
6
+ */
7
+
8
+ // =============================================================================
9
+ // OPERATION NAMES (only implemented ops)
10
+ // =============================================================================
11
+
12
+ export const OPS = {
13
+ // Setup
14
+ SETUP_STATUS: 'setup.status',
15
+
16
+ // Runtime
17
+ RUNTIME_INFO: 'runtime.info',
18
+
19
+ // Auth
20
+ AUTH_SET: 'auth.set',
21
+
22
+ // Node Session
23
+ NODE_SESSION_ENSURE_IDENTITY: 'nodeSession.ensureIdentity',
24
+ NODE_SESSION_BOOTSTRAP: 'nodeSession.bootstrap',
25
+ NODE_SESSION_STATUS: 'nodeSession.status',
26
+
27
+ // Node Credentials (keychain-stored)
28
+ NODE_CREDENTIALS_SET: 'nodeCredentials.set',
29
+ NODE_CREDENTIALS_STATUS: 'nodeCredentials.status',
30
+ NODE_CREDENTIALS_CLEAR: 'nodeCredentials.clear',
31
+
32
+ // Runner
33
+ RUNNER_STATUS: 'runner.status',
34
+ RUNNER_TASK_STATS: 'runner.taskStats',
35
+
36
+ // Workflow Runs (proxied via Rust)
37
+ WORKFLOW_RUNS_CREATE: 'workflowRuns.create',
38
+ WORKFLOW_RUNS_GET: 'workflowRuns.get',
39
+
40
+ // Auth (proxied via Rust)
41
+ AUTH_LOGIN: 'auth.login',
42
+ AUTH_REFRESH: 'auth.refresh',
43
+ AUTH_LOGOUT: 'auth.logout',
44
+
45
+ // Home
46
+ HOME_STATUS: 'home.status',
47
+ HOME_GRANT: 'home.grant',
48
+
49
+ // Paths
50
+ PATHS_CHECK: 'paths.check',
51
+ PATHS_LIST: 'paths.list',
52
+ PATHS_GET: 'paths.get',
53
+ PATHS_REQUEST: 'paths.request',
54
+ PATHS_REMOVE: 'paths.remove',
55
+
56
+ // Vault - Status/Capabilities
57
+ VAULT_STATUS: 'vault.status',
58
+ VAULT_CAPABILITIES: 'vault.capabilities',
59
+
60
+ // Vault - Secrets
61
+ VAULT_SECRETS_LIST: 'vault.secrets.list',
62
+ VAULT_SECRETS_GET: 'vault.secrets.get',
63
+ VAULT_SECRETS_CREATE: 'vault.secrets.create',
64
+ VAULT_SECRETS_UPDATE: 'vault.secrets.update',
65
+ VAULT_SECRETS_DELETE: 'vault.secrets.delete',
66
+ VAULT_SECRETS_UPSERT: 'vault.secrets.upsert',
67
+
68
+ // Vault - Bundles
69
+ VAULT_BUNDLES_LIST: 'vault.bundles.list',
70
+ VAULT_BUNDLES_GET: 'vault.bundles.get',
71
+ VAULT_BUNDLES_CREATE: 'vault.bundles.create',
72
+ VAULT_BUNDLES_RENAME: 'vault.bundles.rename',
73
+ VAULT_BUNDLES_DELETE: 'vault.bundles.delete',
74
+ VAULT_BUNDLES_LIST_SECRETS: 'vault.bundles.listSecrets',
75
+ VAULT_BUNDLES_ADD_SECRET: 'vault.bundles.addSecret',
76
+ VAULT_BUNDLES_REMOVE_SECRET: 'vault.bundles.removeSecret',
77
+
78
+ // Vault - Files
79
+ VAULT_FILES_WRITE_TEXT: 'vault.files.writeText',
80
+ VAULT_FILES_WRITE_BYTES: 'vault.files.writeBytes',
81
+ VAULT_FILES_READ_TEXT: 'vault.files.readText',
82
+ VAULT_FILES_READ_BYTES: 'vault.files.readBytes',
83
+ VAULT_FILES_LIST: 'vault.files.list',
84
+ VAULT_FILES_EXISTS: 'vault.files.exists',
85
+ VAULT_FILES_DELETE: 'vault.files.delete',
86
+ VAULT_FILES_MKDIR: 'vault.files.mkdir',
87
+ VAULT_FILES_MOVE: 'vault.files.move',
88
+
89
+ // Vault - Injection (DEFERRED - returns NOT_IMPLEMENTED)
90
+ VAULT_ATTACH_SECRETS_TO_CONNECTOR: 'vault.attachSecretsToConnector',
91
+ VAULT_INJECT_SECRETS_INTO_RUN: 'vault.injectSecretsIntoRun',
92
+
93
+ // Vault - Audit
94
+ VAULT_AUDIT_LIST: 'vault.audit.list',
95
+ } as const;
96
+
97
+ export type OpName = (typeof OPS)[keyof typeof OPS];
98
+
99
+ // =============================================================================
100
+ // ERROR CODES
101
+ // =============================================================================
102
+
103
+ export const ERROR_CODES = {
104
+ NOT_CONNECTED: 'NOT_CONNECTED',
105
+ NOT_AUTHENTICATED: 'NOT_AUTHENTICATED',
106
+ HOME_GRANT_REQUIRED: 'HOME_GRANT_REQUIRED',
107
+ INVALID_OP: 'INVALID_OP',
108
+ INVALID_PAYLOAD: 'INVALID_PAYLOAD',
109
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
110
+
111
+ // Vault
112
+ VAULT_NOT_INITIALIZED: 'VAULT_NOT_INITIALIZED',
113
+ VAULT_ERROR: 'VAULT_ERROR',
114
+ SECRET_NOT_FOUND: 'SECRET_NOT_FOUND',
115
+ SECRET_ALREADY_EXISTS: 'SECRET_ALREADY_EXISTS',
116
+ BUNDLE_NOT_FOUND: 'BUNDLE_NOT_FOUND',
117
+ BUNDLE_ALREADY_EXISTS: 'BUNDLE_ALREADY_EXISTS',
118
+
119
+ // Files
120
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
121
+ FILE_ALREADY_EXISTS: 'FILE_ALREADY_EXISTS',
122
+ DIRECTORY_NOT_EMPTY: 'DIRECTORY_NOT_EMPTY',
123
+ INVALID_PATH: 'INVALID_PATH',
124
+ PATH_TRAVERSAL_DENIED: 'PATH_TRAVERSAL_DENIED',
125
+
126
+ // Deferred
127
+ NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
128
+
129
+ // Credentials
130
+ INVALID_NODE_ID: 'INVALID_NODE_ID',
131
+ INVALID_NODE_SECRET: 'INVALID_NODE_SECRET',
132
+ CREDENTIALS_STORE_ERROR: 'CREDENTIALS_STORE_ERROR',
133
+ CREDENTIALS_CLEAR_ERROR: 'CREDENTIALS_CLEAR_ERROR',
134
+ CREDENTIALS_NOT_CONFIGURED: 'CREDENTIALS_NOT_CONFIGURED',
135
+ } as const;
136
+
137
+ export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
138
+
139
+ // =============================================================================
140
+ // CONTRACT VERSION
141
+ // =============================================================================
142
+
143
+ export const CONTRACT_VERSION = 2;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * EKKA Error Classes
3
+ *
4
+ * Custom error types for the EKKA client library.
5
+ */
6
+
7
+ import { ERROR_CODES } from './constants';
8
+
9
+ /**
10
+ * Base error class for all EKKA errors.
11
+ */
12
+ export class EkkaError extends Error {
13
+ readonly code: string;
14
+
15
+ constructor(message: string, code: string) {
16
+ super(message);
17
+ this.name = 'EkkaError';
18
+ this.code = code;
19
+ Object.setPrototypeOf(this, new.target.prototype);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Thrown when an operation is attempted without a connection.
25
+ */
26
+ export class EkkaNotConnectedError extends EkkaError {
27
+ constructor() {
28
+ super('Not connected. Call ekka.connect() first.', ERROR_CODES.NOT_CONNECTED);
29
+ this.name = 'EkkaNotConnectedError';
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Thrown when connection fails.
35
+ */
36
+ export class EkkaConnectionError extends EkkaError {
37
+ constructor(message: string) {
38
+ super(message, 'CONNECTION_ERROR');
39
+ this.name = 'EkkaConnectionError';
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Thrown when an API operation fails.
45
+ */
46
+ export class EkkaApiError extends EkkaError {
47
+ readonly httpStatus?: number;
48
+
49
+ constructor(message: string, code: string, httpStatus?: number) {
50
+ super(message, code);
51
+ this.name = 'EkkaApiError';
52
+ this.httpStatus = httpStatus;
53
+ }
54
+ }