Skip to content

Commit 1a4eb81

Browse files
committed
fix(app): connect desktop bridge and native keyring
1 parent a7637ba commit 1a4eb81

6 files changed

Lines changed: 79 additions & 27 deletions

File tree

packages/app/src/app/core/api/host-bridge.client.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,56 @@ import type {
77

88
@Injectable({ providedIn: "root" })
99
export class HostBridgeClient implements IHostBridge {
10+
private nextId = 0;
11+
private readonly listeners = new Set<(event: unknown) => void>();
12+
private readonly pending = new Map<string, { resolve(value: unknown): void; reject(error: Error): void }>();
13+
1014
request<K extends keyof IHostRequestMap>(
1115
operation: K,
1216
input: IHostRequestMap[K],
1317
): Promise<IHostResponseMap[K]> {
14-
void operation;
15-
void input;
16-
return Promise.reject(new Error("Host bridge is not connected"));
18+
const id = `ui-${++this.nextId}`;
19+
const frame = JSON.stringify({ jsonrpc: "2.0", id, method: operation, params: input ?? {} });
20+
const tauri = (globalThis as { __TAURI__?: { invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown>; event?: { listen?: (name: string, callback: (event: { payload: unknown }) => void) => Promise<() => void> } } }).__TAURI__;
21+
if (tauri?.invoke) return this.requestTauri(id, frame, tauri) as Promise<IHostResponseMap[K]>;
22+
return this.requestBrowser(frame) as Promise<IHostResponseMap[K]>;
1723
}
1824

1925
subscribe(listener: (event: unknown) => void): () => void {
20-
void listener;
21-
return () => undefined;
26+
this.listeners.add(listener);
27+
const tauri = (globalThis as { __TAURI__?: { event?: { listen?: (name: string, callback: (event: { payload: unknown }) => void) => Promise<() => void> } } }).__TAURI__;
28+
void tauri?.event?.listen?.("tanit://ipc-message", (event) => {
29+
this.dispatchEvent(event.payload);
30+
listener(event.payload);
31+
});
32+
return () => this.listeners.delete(listener);
33+
}
34+
35+
private async requestTauri(id: string, frame: string, tauri: { invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown>; event?: { listen?: (name: string, callback: (event: { payload: unknown }) => void) => Promise<() => void> } }): Promise<unknown> {
36+
if (!tauri.invoke || !tauri.event?.listen) throw new Error("Tauri bridge is incomplete");
37+
const response = new Promise<unknown>((resolve, reject) => {
38+
this.pending.set(id, { resolve, reject });
39+
});
40+
await tauri.invoke("send_to_sidecar", { frame });
41+
return response;
42+
}
43+
44+
private async requestBrowser(frame: string): Promise<unknown> {
45+
const token = typeof document !== "undefined" ? document.documentElement.dataset.tanitToken : undefined;
46+
const response = await fetch("/api", { method: "POST", headers: { "content-type": "application/json", ...(token ? { "x-tanit-token": token } : {}) }, body: frame });
47+
const payload = await response.json() as { result?: unknown; error?: { message?: string } };
48+
if (!response.ok || payload.error) throw new Error(payload.error?.message ?? "Host request failed");
49+
return payload.result;
50+
}
51+
52+
private dispatchEvent(event: unknown): void {
53+
if (!event || typeof event !== "object") return;
54+
const payload = event as { id?: unknown; result?: unknown; error?: { message?: string } };
55+
if (typeof payload.id !== "string") return;
56+
const pending = this.pending.get(payload.id);
57+
if (!pending) return;
58+
this.pending.delete(payload.id);
59+
if (payload.error) pending.reject(new Error(payload.error.message ?? "Host request failed"));
60+
else pending.resolve(payload.result);
2261
}
2362
}

packages/contracts/interfaces/ui/host-bridge.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface IHostRequestMap {
1212
validate: { readonly projectRoot: string };
1313
check: { readonly projectRoot: string };
1414
sync: { readonly projectRoot: string };
15-
push: { readonly projectRoot: string };
15+
push: { readonly projectRoot: string; readonly workspace?: string; readonly dryRun?: boolean; readonly apiKey?: string };
1616
settings: undefined;
1717
}
1818

@@ -28,7 +28,7 @@ export interface IHostResponseMap {
2828
validate: { readonly valid: boolean; readonly issues: ReadonlyArray<string> };
2929
check: { readonly passed: boolean; readonly issues: ReadonlyArray<string> };
3030
sync: { readonly synced: boolean };
31-
push: { readonly pushed: boolean };
31+
push: { readonly pushed: boolean; readonly diagnostics?: ReadonlyArray<string> };
3232
settings: { readonly saved: boolean };
3333
}
3434

packages/core/session/snapshot-hash.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ export function canonicalSnapshotJson(snapshot: ICanonicalSnapshot): string {
5454

5555
export function snapshotSha256(snapshot: ICanonicalSnapshot): string {
5656
return createHash("sha256")
57-
.update(canonicalSnapshotJson(snapshot), "utf8")
57+
.update(canonicalSnapshotJson(snapshot))
5858
.digest("hex");
5959
}
6060

6161
export function snapshotHashFromJson(json: string): string {
62-
return createHash("sha256").update(json, "utf8").digest("hex");
62+
return createHash("sha256").update(json).digest("hex");
6363
}

packages/desktop/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tauri-build = { version = "2", features = [] }
2424
tauri = { version = "2", features = [] }
2525
tauri-plugin-dialog = "2"
2626
tauri-plugin-shell = "2"
27+
keyring = "3"
2728

2829
[profile.release]
2930
# El binario de la ventana es un envoltorio: lo que pesa es el sidecar.

packages/desktop/src/main.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ mod bridge;
2222
mod dialogs;
2323
mod drag_drop;
2424
mod sidecar;
25+
mod secure_storage;
2526

2627
use std::process::Child;
2728
use std::sync::Mutex;
2829

2930
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
3031

3132
use bridge::Bridge;
33+
use secure_storage::SecureStorage;
3234
use sidecar::spawn as spawn_sidecar;
3335

3436
/// Posee el `Child` del sidecar para matarlo al destruirse la
@@ -40,8 +42,9 @@ fn main() {
4042
tauri::Builder::default()
4143
.plugin(tauri_plugin_dialog::init())
4244
.plugin(tauri_plugin_shell::init())
43-
.invoke_handler(tauri::generate_handler![bridge::send_to_sidecar])
45+
.invoke_handler(tauri::generate_handler![bridge::send_to_sidecar, secure_storage_save, secure_storage_retrieve, secure_storage_delete])
4446
.manage(Sidecar(Mutex::new(None)))
47+
.manage(SecureStorage::default())
4548
.setup(|app| {
4649
// 1. Sidecar: `apisrc serve --stdio`, stdin/stdout para
4750
// el bridge; stderr va a un hilo logger propio
@@ -86,6 +89,21 @@ fn main() {
8689
.expect("no se pudo arrancar la ventana");
8790
}
8891

92+
#[tauri::command]
93+
fn secure_storage_save(storage: tauri::State<'_, SecureStorage>, service: String, value: String) -> Result<(), String> {
94+
storage.save(service, value)
95+
}
96+
97+
#[tauri::command]
98+
fn secure_storage_retrieve(storage: tauri::State<'_, SecureStorage>, service: String) -> Result<Option<String>, String> {
99+
storage.retrieve(&service)
100+
}
101+
102+
#[tauri::command]
103+
fn secure_storage_delete(storage: tauri::State<'_, SecureStorage>, service: String) -> Result<(), String> {
104+
storage.delete(&service)
105+
}
106+
89107
#[cfg(test)]
90108
mod tests {
91109
/// Documenta el reparto de módulos. Si alguien mueve un `mod`
Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
11
//! Native secure-storage boundary. Production wiring can replace the
22
//! implementation with the platform keyring without changing the webview API.
33
4-
use std::collections::HashMap;
5-
use std::sync::Mutex;
6-
7-
pub struct SecureStorage {
8-
values: Mutex<HashMap<String, String>>,
9-
}
10-
11-
impl Default for SecureStorage {
12-
fn default() -> Self {
13-
Self { values: Mutex::new(HashMap::new()) }
14-
}
15-
}
4+
use keyring::Entry;
165

176
impl SecureStorage {
187
pub fn save(&self, service: String, value: String) -> Result<(), String> {
19-
self.values.lock().map_err(|_| "secure storage unavailable".to_string())?.insert(service, value);
20-
Ok(())
8+
Entry::new("tanit", &service).map_err(|error| error.to_string())?.set_password(&value).map_err(|error| error.to_string())
219
}
2210

2311
pub fn retrieve(&self, service: &str) -> Result<Option<String>, String> {
24-
Ok(self.values.lock().map_err(|_| "secure storage unavailable".to_string())?.get(service).cloned())
12+
match Entry::new("tanit", service).map_err(|error| error.to_string())?.get_password() {
13+
Ok(value) => Ok(Some(value)),
14+
Err(keyring::Error::NoEntry) => Ok(None),
15+
Err(error) => Err(error.to_string()),
16+
}
2517
}
2618

2719
pub fn delete(&self, service: &str) -> Result<(), String> {
28-
self.values.lock().map_err(|_| "secure storage unavailable".to_string())?.remove(service);
29-
Ok(())
20+
match Entry::new("tanit", service).map_err(|error| error.to_string())?.delete_credential() {
21+
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
22+
Err(error) => Err(error.to_string()),
23+
}
3024
}
3125
}

0 commit comments

Comments
 (0)