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,680 @@
1
+ //! Vault operation handlers
2
+ //!
3
+ //! Thin wrappers that call SDK vault operations.
4
+ //! Each handler is 5-15 lines max per architecture rules.
5
+
6
+ use crate::state::EngineState;
7
+ use crate::types::EngineResponse;
8
+ use ekka_sdk_core::ekka_ops::vault;
9
+ use serde_json::{json, Value};
10
+
11
+ // =============================================================================
12
+ // Status Handlers
13
+ // =============================================================================
14
+
15
+ /// Handle vault.status
16
+ pub fn handle_status(state: &EngineState) -> EngineResponse {
17
+ let ctx = match state.to_runtime_context() {
18
+ Some(c) => c,
19
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
20
+ };
21
+
22
+ match vault::status(&ctx) {
23
+ Ok(status) => EngineResponse::ok(json!(status)),
24
+ Err(e) => EngineResponse::err(e.code, &e.message),
25
+ }
26
+ }
27
+
28
+ /// Handle vault.capabilities
29
+ pub fn handle_capabilities(state: &EngineState) -> EngineResponse {
30
+ let ctx = match state.to_runtime_context() {
31
+ Some(c) => c,
32
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
33
+ };
34
+
35
+ match vault::capabilities(&ctx) {
36
+ Ok(caps) => EngineResponse::ok(json!(caps)),
37
+ Err(e) => EngineResponse::err(e.code, &e.message),
38
+ }
39
+ }
40
+
41
+ // =============================================================================
42
+ // Secrets Handlers
43
+ // =============================================================================
44
+
45
+ /// Handle vault.secrets.list
46
+ pub fn handle_secrets_list(payload: &Value, state: &EngineState) -> EngineResponse {
47
+ let ctx = match state.to_runtime_context() {
48
+ Some(c) => c,
49
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
50
+ };
51
+
52
+ let opts: Option<vault::SecretListOptions> = payload
53
+ .get("opts")
54
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
55
+
56
+ match vault::secrets::list(&ctx, state.vault_cache(), opts) {
57
+ Ok(secrets) => EngineResponse::ok(json!({ "secrets": secrets })),
58
+ Err(e) => EngineResponse::err(e.code, &e.message),
59
+ }
60
+ }
61
+
62
+ /// Handle vault.secrets.get
63
+ pub fn handle_secrets_get(payload: &Value, state: &EngineState) -> EngineResponse {
64
+ let ctx = match state.to_runtime_context() {
65
+ Some(c) => c,
66
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
67
+ };
68
+
69
+ let id = match payload.get("id").and_then(|v| v.as_str()) {
70
+ Some(id) => id,
71
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'id'"),
72
+ };
73
+
74
+ match vault::secrets::get(&ctx, state.vault_cache(), id) {
75
+ Ok(secret) => EngineResponse::ok(json!(secret)),
76
+ Err(e) => EngineResponse::err(e.code, &e.message),
77
+ }
78
+ }
79
+
80
+ /// Handle vault.secrets.create
81
+ pub fn handle_secrets_create(payload: &Value, state: &EngineState) -> EngineResponse {
82
+ let ctx = match state.to_runtime_context() {
83
+ Some(c) => c,
84
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
85
+ };
86
+
87
+ let input: vault::SecretCreateInput = match serde_json::from_value(payload.clone()) {
88
+ Ok(i) => i,
89
+ Err(e) => return EngineResponse::err("INVALID_PAYLOAD", &e.to_string()),
90
+ };
91
+
92
+ match vault::secrets::create(&ctx, state.vault_cache(), input) {
93
+ Ok(secret) => EngineResponse::ok(json!(secret)),
94
+ Err(e) => EngineResponse::err(e.code, &e.message),
95
+ }
96
+ }
97
+
98
+ /// Handle vault.secrets.update
99
+ pub fn handle_secrets_update(payload: &Value, state: &EngineState) -> EngineResponse {
100
+ let ctx = match state.to_runtime_context() {
101
+ Some(c) => c,
102
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
103
+ };
104
+
105
+ let id = match payload.get("id").and_then(|v| v.as_str()) {
106
+ Some(id) => id,
107
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'id'"),
108
+ };
109
+
110
+ let input: vault::SecretUpdateInput = match payload.get("input")
111
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
112
+ {
113
+ Some(i) => i,
114
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing or invalid 'input'"),
115
+ };
116
+
117
+ match vault::secrets::update(&ctx, state.vault_cache(), id, input) {
118
+ Ok(secret) => EngineResponse::ok(json!(secret)),
119
+ Err(e) => EngineResponse::err(e.code, &e.message),
120
+ }
121
+ }
122
+
123
+ /// Handle vault.secrets.delete
124
+ pub fn handle_secrets_delete(payload: &Value, state: &EngineState) -> EngineResponse {
125
+ let ctx = match state.to_runtime_context() {
126
+ Some(c) => c,
127
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
128
+ };
129
+
130
+ let id = match payload.get("id").and_then(|v| v.as_str()) {
131
+ Some(id) => id,
132
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'id'"),
133
+ };
134
+
135
+ match vault::secrets::delete(&ctx, state.vault_cache(), id) {
136
+ Ok(deleted) => EngineResponse::ok(json!({ "deleted": deleted })),
137
+ Err(e) => EngineResponse::err(e.code, &e.message),
138
+ }
139
+ }
140
+
141
+ /// Handle vault.secrets.upsert
142
+ pub fn handle_secrets_upsert(payload: &Value, state: &EngineState) -> EngineResponse {
143
+ let ctx = match state.to_runtime_context() {
144
+ Some(c) => c,
145
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
146
+ };
147
+
148
+ let input: vault::SecretCreateInput = match serde_json::from_value(payload.clone()) {
149
+ Ok(i) => i,
150
+ Err(e) => return EngineResponse::err("INVALID_PAYLOAD", &e.to_string()),
151
+ };
152
+
153
+ match vault::secrets::upsert(&ctx, state.vault_cache(), input) {
154
+ Ok(secret) => EngineResponse::ok(json!(secret)),
155
+ Err(e) => EngineResponse::err(e.code, &e.message),
156
+ }
157
+ }
158
+
159
+ // =============================================================================
160
+ // Bundles Handlers
161
+ // =============================================================================
162
+
163
+ /// Handle vault.bundles.list
164
+ pub fn handle_bundles_list(payload: &Value, state: &EngineState) -> EngineResponse {
165
+ let ctx = match state.to_runtime_context() {
166
+ Some(c) => c,
167
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
168
+ };
169
+
170
+ let opts: Option<vault::BundleListOptions> = payload
171
+ .get("opts")
172
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
173
+
174
+ match vault::bundles::list(&ctx, state.vault_cache(), opts) {
175
+ Ok(bundles) => EngineResponse::ok(json!({ "bundles": bundles })),
176
+ Err(e) => EngineResponse::err(e.code, &e.message),
177
+ }
178
+ }
179
+
180
+ /// Handle vault.bundles.get
181
+ pub fn handle_bundles_get(payload: &Value, state: &EngineState) -> EngineResponse {
182
+ let ctx = match state.to_runtime_context() {
183
+ Some(c) => c,
184
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
185
+ };
186
+
187
+ let id = match payload.get("id").and_then(|v| v.as_str()) {
188
+ Some(id) => id,
189
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'id'"),
190
+ };
191
+
192
+ match vault::bundles::get(&ctx, state.vault_cache(), id) {
193
+ Ok(bundle) => EngineResponse::ok(json!(bundle)),
194
+ Err(e) => EngineResponse::err(e.code, &e.message),
195
+ }
196
+ }
197
+
198
+ /// Handle vault.bundles.create
199
+ pub fn handle_bundles_create(payload: &Value, state: &EngineState) -> EngineResponse {
200
+ let ctx = match state.to_runtime_context() {
201
+ Some(c) => c,
202
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
203
+ };
204
+
205
+ let input: vault::BundleCreateInput = match serde_json::from_value(payload.clone()) {
206
+ Ok(i) => i,
207
+ Err(e) => return EngineResponse::err("INVALID_PAYLOAD", &e.to_string()),
208
+ };
209
+
210
+ match vault::bundles::create(&ctx, state.vault_cache(), input) {
211
+ Ok(bundle) => EngineResponse::ok(json!(bundle)),
212
+ Err(e) => EngineResponse::err(e.code, &e.message),
213
+ }
214
+ }
215
+
216
+ /// Handle vault.bundles.rename
217
+ pub fn handle_bundles_rename(payload: &Value, state: &EngineState) -> EngineResponse {
218
+ let ctx = match state.to_runtime_context() {
219
+ Some(c) => c,
220
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
221
+ };
222
+
223
+ let id = match payload.get("id").and_then(|v| v.as_str()) {
224
+ Some(id) => id,
225
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'id'"),
226
+ };
227
+
228
+ let name = match payload.get("name").and_then(|v| v.as_str()) {
229
+ Some(n) => n,
230
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'name'"),
231
+ };
232
+
233
+ match vault::bundles::rename(&ctx, state.vault_cache(), id, name) {
234
+ Ok(bundle) => EngineResponse::ok(json!(bundle)),
235
+ Err(e) => EngineResponse::err(e.code, &e.message),
236
+ }
237
+ }
238
+
239
+ /// Handle vault.bundles.delete
240
+ pub fn handle_bundles_delete(payload: &Value, state: &EngineState) -> EngineResponse {
241
+ let ctx = match state.to_runtime_context() {
242
+ Some(c) => c,
243
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
244
+ };
245
+
246
+ let id = match payload.get("id").and_then(|v| v.as_str()) {
247
+ Some(id) => id,
248
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'id'"),
249
+ };
250
+
251
+ match vault::bundles::delete(&ctx, state.vault_cache(), id) {
252
+ Ok(deleted) => EngineResponse::ok(json!({ "deleted": deleted })),
253
+ Err(e) => EngineResponse::err(e.code, &e.message),
254
+ }
255
+ }
256
+
257
+ /// Handle vault.bundles.listSecrets
258
+ pub fn handle_bundles_list_secrets(payload: &Value, state: &EngineState) -> EngineResponse {
259
+ let ctx = match state.to_runtime_context() {
260
+ Some(c) => c,
261
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
262
+ };
263
+
264
+ let bundle_id = match payload.get("bundleId").and_then(|v| v.as_str()) {
265
+ Some(id) => id,
266
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'bundleId'"),
267
+ };
268
+
269
+ let opts: Option<vault::SecretListOptions> = payload
270
+ .get("opts")
271
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
272
+
273
+ match vault::bundles::list_secrets(&ctx, state.vault_cache(), bundle_id, opts) {
274
+ Ok(secrets) => EngineResponse::ok(json!({ "secrets": secrets })),
275
+ Err(e) => EngineResponse::err(e.code, &e.message),
276
+ }
277
+ }
278
+
279
+ /// Handle vault.bundles.addSecret
280
+ pub fn handle_bundles_add_secret(payload: &Value, state: &EngineState) -> EngineResponse {
281
+ let ctx = match state.to_runtime_context() {
282
+ Some(c) => c,
283
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
284
+ };
285
+
286
+ let bundle_id = match payload.get("bundleId").and_then(|v| v.as_str()) {
287
+ Some(id) => id,
288
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'bundleId'"),
289
+ };
290
+
291
+ let secret_id = match payload.get("secretId").and_then(|v| v.as_str()) {
292
+ Some(id) => id,
293
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'secretId'"),
294
+ };
295
+
296
+ match vault::bundles::add_secret(&ctx, state.vault_cache(), bundle_id, secret_id) {
297
+ Ok(bundle) => EngineResponse::ok(json!(bundle)),
298
+ Err(e) => EngineResponse::err(e.code, &e.message),
299
+ }
300
+ }
301
+
302
+ /// Handle vault.bundles.removeSecret
303
+ pub fn handle_bundles_remove_secret(payload: &Value, state: &EngineState) -> EngineResponse {
304
+ let ctx = match state.to_runtime_context() {
305
+ Some(c) => c,
306
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
307
+ };
308
+
309
+ let bundle_id = match payload.get("bundleId").and_then(|v| v.as_str()) {
310
+ Some(id) => id,
311
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'bundleId'"),
312
+ };
313
+
314
+ let secret_id = match payload.get("secretId").and_then(|v| v.as_str()) {
315
+ Some(id) => id,
316
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'secretId'"),
317
+ };
318
+
319
+ match vault::bundles::remove_secret(&ctx, state.vault_cache(), bundle_id, secret_id) {
320
+ Ok(bundle) => EngineResponse::ok(json!(bundle)),
321
+ Err(e) => EngineResponse::err(e.code, &e.message),
322
+ }
323
+ }
324
+
325
+ // =============================================================================
326
+ // Files Handlers
327
+ // =============================================================================
328
+
329
+ /// Handle vault.files.writeText
330
+ pub fn handle_files_write_text(payload: &Value, state: &EngineState) -> EngineResponse {
331
+ let ctx = match state.to_runtime_context() {
332
+ Some(c) => c,
333
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
334
+ };
335
+
336
+ let path = match payload.get("path").and_then(|v| v.as_str()) {
337
+ Some(p) => p,
338
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
339
+ };
340
+
341
+ let content = match payload.get("content").and_then(|v| v.as_str()) {
342
+ Some(c) => c,
343
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'content'"),
344
+ };
345
+
346
+ let opts: Option<vault::FileOptions> = payload
347
+ .get("opts")
348
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
349
+
350
+ match vault::files::write_text(&ctx, state.vault_cache(), path, content, opts) {
351
+ Ok(()) => EngineResponse::ok(json!({ "written": true })),
352
+ Err(e) => EngineResponse::err(e.code, &e.message),
353
+ }
354
+ }
355
+
356
+ /// Handle vault.files.writeBytes
357
+ pub fn handle_files_write_bytes(payload: &Value, state: &EngineState) -> EngineResponse {
358
+ let ctx = match state.to_runtime_context() {
359
+ Some(c) => c,
360
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
361
+ };
362
+
363
+ let path = match payload.get("path").and_then(|v| v.as_str()) {
364
+ Some(p) => p,
365
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
366
+ };
367
+
368
+ // contentBytes is expected as base64 encoded string
369
+ let content_b64 = match payload.get("contentBytes").and_then(|v| v.as_str()) {
370
+ Some(c) => c,
371
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'contentBytes'"),
372
+ };
373
+
374
+ let content = match base64_decode(content_b64) {
375
+ Ok(c) => c,
376
+ Err(e) => return EngineResponse::err("INVALID_PAYLOAD", &format!("Invalid base64: {}", e)),
377
+ };
378
+
379
+ let opts: Option<vault::FileOptions> = payload
380
+ .get("opts")
381
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
382
+
383
+ match vault::files::write_bytes(&ctx, state.vault_cache(), path, &content, opts) {
384
+ Ok(()) => EngineResponse::ok(json!({ "written": true })),
385
+ Err(e) => EngineResponse::err(e.code, &e.message),
386
+ }
387
+ }
388
+
389
+ /// Handle vault.files.readText
390
+ pub fn handle_files_read_text(payload: &Value, state: &EngineState) -> EngineResponse {
391
+ let ctx = match state.to_runtime_context() {
392
+ Some(c) => c,
393
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
394
+ };
395
+
396
+ let path = match payload.get("path").and_then(|v| v.as_str()) {
397
+ Some(p) => p,
398
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
399
+ };
400
+
401
+ let opts: Option<vault::FileOptions> = payload
402
+ .get("opts")
403
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
404
+
405
+ match vault::files::read_text(&ctx, state.vault_cache(), path, opts) {
406
+ Ok(content) => EngineResponse::ok(json!({ "content": content })),
407
+ Err(e) => EngineResponse::err(e.code, &e.message),
408
+ }
409
+ }
410
+
411
+ /// Handle vault.files.readBytes
412
+ pub fn handle_files_read_bytes(payload: &Value, state: &EngineState) -> EngineResponse {
413
+ let ctx = match state.to_runtime_context() {
414
+ Some(c) => c,
415
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
416
+ };
417
+
418
+ let path = match payload.get("path").and_then(|v| v.as_str()) {
419
+ Some(p) => p,
420
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
421
+ };
422
+
423
+ let opts: Option<vault::FileOptions> = payload
424
+ .get("opts")
425
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
426
+
427
+ match vault::files::read_bytes(&ctx, state.vault_cache(), path, opts) {
428
+ Ok(content) => {
429
+ let content_b64 = base64_encode(&content);
430
+ EngineResponse::ok(json!({ "contentBytes": content_b64 }))
431
+ }
432
+ Err(e) => EngineResponse::err(e.code, &e.message),
433
+ }
434
+ }
435
+
436
+ /// Handle vault.files.list
437
+ pub fn handle_files_list(payload: &Value, state: &EngineState) -> EngineResponse {
438
+ let ctx = match state.to_runtime_context() {
439
+ Some(c) => c,
440
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
441
+ };
442
+
443
+ let dir_path = payload.get("dir").and_then(|v| v.as_str()).unwrap_or("");
444
+
445
+ let opts: Option<vault::FileListOptions> = payload
446
+ .get("opts")
447
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
448
+
449
+ match vault::files::list(&ctx, state.vault_cache(), dir_path, opts) {
450
+ Ok(entries) => EngineResponse::ok(json!({ "entries": entries })),
451
+ Err(e) => EngineResponse::err(e.code, &e.message),
452
+ }
453
+ }
454
+
455
+ /// Handle vault.files.exists
456
+ pub fn handle_files_exists(payload: &Value, state: &EngineState) -> EngineResponse {
457
+ let ctx = match state.to_runtime_context() {
458
+ Some(c) => c,
459
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
460
+ };
461
+
462
+ let path = match payload.get("path").and_then(|v| v.as_str()) {
463
+ Some(p) => p,
464
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
465
+ };
466
+
467
+ let opts: Option<vault::FileOptions> = payload
468
+ .get("opts")
469
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
470
+
471
+ match vault::files::exists(&ctx, state.vault_cache(), path, opts) {
472
+ Ok(exists) => EngineResponse::ok(json!({ "exists": exists })),
473
+ Err(e) => EngineResponse::err(e.code, &e.message),
474
+ }
475
+ }
476
+
477
+ /// Handle vault.files.delete
478
+ pub fn handle_files_delete(payload: &Value, state: &EngineState) -> EngineResponse {
479
+ let ctx = match state.to_runtime_context() {
480
+ Some(c) => c,
481
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
482
+ };
483
+
484
+ let path = match payload.get("path").and_then(|v| v.as_str()) {
485
+ Some(p) => p,
486
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
487
+ };
488
+
489
+ let opts: Option<vault::FileDeleteOptions> = payload
490
+ .get("opts")
491
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
492
+
493
+ match vault::files::delete(&ctx, state.vault_cache(), path, opts) {
494
+ Ok(deleted) => EngineResponse::ok(json!({ "deleted": deleted })),
495
+ Err(e) => EngineResponse::err(e.code, &e.message),
496
+ }
497
+ }
498
+
499
+ /// Handle vault.files.mkdir
500
+ pub fn handle_files_mkdir(payload: &Value, state: &EngineState) -> EngineResponse {
501
+ let ctx = match state.to_runtime_context() {
502
+ Some(c) => c,
503
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
504
+ };
505
+
506
+ let path = match payload.get("path").and_then(|v| v.as_str()) {
507
+ Some(p) => p,
508
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'path'"),
509
+ };
510
+
511
+ let opts: Option<vault::FileOptions> = payload
512
+ .get("opts")
513
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
514
+
515
+ match vault::files::mkdir(&ctx, state.vault_cache(), path, opts) {
516
+ Ok(()) => EngineResponse::ok(json!({ "created": true })),
517
+ Err(e) => EngineResponse::err(e.code, &e.message),
518
+ }
519
+ }
520
+
521
+ /// Handle vault.files.move
522
+ pub fn handle_files_move(payload: &Value, state: &EngineState) -> EngineResponse {
523
+ let ctx = match state.to_runtime_context() {
524
+ Some(c) => c,
525
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
526
+ };
527
+
528
+ let from = match payload.get("from").and_then(|v| v.as_str()) {
529
+ Some(p) => p,
530
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'from'"),
531
+ };
532
+
533
+ let to = match payload.get("to").and_then(|v| v.as_str()) {
534
+ Some(p) => p,
535
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'to'"),
536
+ };
537
+
538
+ let opts: Option<vault::FileOptions> = payload
539
+ .get("opts")
540
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
541
+
542
+ match vault::files::move_file(&ctx, state.vault_cache(), from, to, opts) {
543
+ Ok(()) => EngineResponse::ok(json!({ "moved": true })),
544
+ Err(e) => EngineResponse::err(e.code, &e.message),
545
+ }
546
+ }
547
+
548
+ // =============================================================================
549
+ // Injection Handlers (DEFERRED - return NOT_IMPLEMENTED)
550
+ // =============================================================================
551
+
552
+ /// Handle vault.attachSecretsToConnector (DEFERRED)
553
+ pub fn handle_attach_secrets_to_connector(payload: &Value, state: &EngineState) -> EngineResponse {
554
+ let ctx = match state.to_runtime_context() {
555
+ Some(c) => c,
556
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
557
+ };
558
+
559
+ let connector_id = match payload.get("connectorId").and_then(|v| v.as_str()) {
560
+ Some(id) => id,
561
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'connectorId'"),
562
+ };
563
+
564
+ let mappings: Vec<vault::SecretRef> = match payload.get("mappings")
565
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
566
+ {
567
+ Some(m) => m,
568
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing or invalid 'mappings'"),
569
+ };
570
+
571
+ match vault::attach_secrets_to_connector(&ctx, connector_id, mappings) {
572
+ Ok(()) => EngineResponse::ok(json!({ "attached": true })),
573
+ Err(e) => EngineResponse::err(e.code, &e.message),
574
+ }
575
+ }
576
+
577
+ /// Handle vault.injectSecretsIntoRun (DEFERRED)
578
+ /// Note: This returns success/failure only, NOT the injected values
579
+ pub fn handle_inject_secrets_into_run(payload: &Value, state: &EngineState) -> EngineResponse {
580
+ let ctx = match state.to_runtime_context() {
581
+ Some(c) => c,
582
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
583
+ };
584
+
585
+ let run_id = match payload.get("runId").and_then(|v| v.as_str()) {
586
+ Some(id) => id,
587
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing 'runId'"),
588
+ };
589
+
590
+ let mappings: Vec<vault::SecretRef> = match payload.get("mappings")
591
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
592
+ {
593
+ Some(m) => m,
594
+ None => return EngineResponse::err("INVALID_PAYLOAD", "Missing or invalid 'mappings'"),
595
+ };
596
+
597
+ match vault::inject_secrets_into_run(&ctx, run_id, mappings) {
598
+ Ok(_env_map) => EngineResponse::ok(json!({ "injected": true })),
599
+ Err(e) => EngineResponse::err(e.code, &e.message),
600
+ }
601
+ }
602
+
603
+ // =============================================================================
604
+ // Audit Handler
605
+ // =============================================================================
606
+
607
+ /// Handle vault.audit.list
608
+ pub fn handle_audit_list(payload: &Value, state: &EngineState) -> EngineResponse {
609
+ let ctx = match state.to_runtime_context() {
610
+ Some(c) => c,
611
+ None => return EngineResponse::err("NOT_CONNECTED", "Engine not initialized"),
612
+ };
613
+
614
+ let opts: Option<vault::AuditListOptions> = payload
615
+ .get("opts")
616
+ .and_then(|v| serde_json::from_value(v.clone()).ok());
617
+
618
+ match vault::audit::list(&ctx, state.vault_cache(), opts) {
619
+ Ok(result) => EngineResponse::ok(json!(result)),
620
+ Err(e) => EngineResponse::err(e.code, &e.message),
621
+ }
622
+ }
623
+
624
+ // =============================================================================
625
+ // Helper Functions
626
+ // =============================================================================
627
+
628
+ /// Simple base64 encoding
629
+ fn base64_encode(data: &[u8]) -> String {
630
+ let mut encoded = String::new();
631
+ const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
632
+
633
+ for chunk in data.chunks(3) {
634
+ let mut n: u32 = 0;
635
+ for (i, &byte) in chunk.iter().enumerate() {
636
+ n |= (byte as u32) << (16 - i * 8);
637
+ }
638
+
639
+ let padding = 3 - chunk.len();
640
+ for i in 0..4 - padding {
641
+ let idx = ((n >> (18 - i * 6)) & 0x3f) as usize;
642
+ encoded.push(ALPHABET[idx] as char);
643
+ }
644
+ for _ in 0..padding {
645
+ encoded.push('=');
646
+ }
647
+ }
648
+ encoded
649
+ }
650
+
651
+ /// Simple base64 decoding
652
+ fn base64_decode(encoded: &str) -> Result<Vec<u8>, &'static str> {
653
+ fn decode_char(c: u8) -> Result<u8, &'static str> {
654
+ match c {
655
+ b'A'..=b'Z' => Ok(c - b'A'),
656
+ b'a'..=b'z' => Ok(c - b'a' + 26),
657
+ b'0'..=b'9' => Ok(c - b'0' + 52),
658
+ b'+' => Ok(62),
659
+ b'/' => Ok(63),
660
+ _ => Err("invalid base64 character"),
661
+ }
662
+ }
663
+
664
+ let input = encoded.trim_end_matches('=');
665
+
666
+ let mut output = Vec::new();
667
+ let mut buffer: u32 = 0;
668
+ let mut bits: u32 = 0;
669
+
670
+ for &byte in input.as_bytes() {
671
+ buffer = (buffer << 6) | decode_char(byte)? as u32;
672
+ bits += 6;
673
+ if bits >= 8 {
674
+ bits -= 8;
675
+ output.push((buffer >> bits) as u8);
676
+ }
677
+ }
678
+
679
+ Ok(output)
680
+ }