@@ -93,6 +93,72 @@ mcp/ # DeepSQL Phase 1 MCP server (Node stdio wrapper around back
9393agent/ # DeepSQL Agent customization (persona, skills, skins; customized Hermes runtime)
9494```
9595
96+ ## Desktop Client (` desktop/ ` )
97+
98+ Cross-platform Electron client for a self-hosted DeepSQL VM. ** Separate npm
99+ project** — ` cd desktop && npm install ` , not part of the root ` package.json ` .
100+
101+ ``` bash
102+ cd desktop
103+ npm start # run npm run dev # run with DevTools
104+ npm run dist:mac # dmg + zip (arm64 + x64), also :win / :linux
105+ npm run smoke -- --url https://deepsql.example.com # headless connection check
106+ npm run selftest:tunnel # end-to-end SSH tunnel test (in-process SSH server)
107+ ```
108+
109+ ** It is a thin client and deliberately does not bundle the React frontend.** It
110+ navigates a ` WebContentsView ` at the real DeepSQL origin, so the UI is always the
111+ version the VM is running — no bundle/backend skew, and no second copy of 40+
112+ tabs to maintain. This works with ** zero backend changes** because
113+ ` docker/nginx/default.conf ` already serves the SPA, ` /api ` and ` /agent-api ` from
114+ one origin: cookies, CORS and SSE behave exactly as in a browser. Do not
115+ "improve" this by bundling ` dist/ ` — that reintroduces CORS, ` SameSite ` , and
116+ version-skew problems the current design does not have.
117+
118+ ** Two transports, one abstraction.** Both resolve to an * origin* , so nothing
119+ downstream of ` desktop/src/main/transport.js ` knows which is in use:
120+
121+ - ** Direct TLS** — the VM's HTTPS origin. Four certificate modes (` system ` ,
122+ ` pinned ` , ` custom-ca ` , ` insecure ` /TOFU), applied to ** both** the Node health
123+ probe and the Chromium session (` tls.applyToSession ` ). Applying it to only one
124+ gives a connection that tests green but renders a certificate error.
125+ - ** SSH tunnel** — ` ssh2 ` local forward, loopback-bound, no ` ssh ` binary needed.
126+ The local port is * sticky* across launches on purpose: the origin includes the
127+ port, and a fresh random port would silently reset the web app's
128+ ` localStorage ` . ` http://127.0.0.1:* ` is a Chromium secure context, so the
129+ backend's ` Secure ` cookies still work over the tunnel. ** Forward to the
130+ frontend container (3000), not a host reverse proxy on :80** — that proxy
131+ matches on ` server_name ` , a tunnel arrives with ` Host: 127.0.0.1:<port> ` ,
132+ and the request lands on the default vhost as a 404 that reads like a broken
133+ backend. The container's nginx uses ` server_name _ ` and answers any Host.
134+
135+ Three non-obvious things, all found the hard way:
136+
137+ 1 . ** ` Client.connect({ privateKey }) ` must get the raw key material, not the
138+ object ` sshUtils.parseKey ` returns.** Handed a parsed key, ssh2 silently
139+ never offers the publickey method and the server replies with a bare
140+ authentication failure — a symptom that points at the VM's ` authorized_keys `
141+ rather than at a type mismatch on our side. ` loadPrivateKey ` parses only to
142+ produce good error messages and returns the buffer.
143+ 2 . ** Authentication succeeding says nothing about forwarding being allowed.**
144+ A hardened sshd (` AllowTcpForwarding no ` ) accepts the login and refuses every
145+ ` direct-tcpip ` channel; the failure otherwise surfaces as "socket hang up" on
146+ the first browser request, pointing nowhere near sshd. ` verifyForwarding() `
147+ opens and closes one channel right after auth and classifies the refusal by
148+ SSH reason code — 1 (` ADMINISTRATIVELY_PROHIBITED ` , verified against real
149+ OpenSSH) names ` AllowTcpForwarding ` , 2 (` CONNECT_FAILED ` ) means nothing is
150+ listening on the remote port.
151+ 3 . ** Only a session that once reached ` ready ` may be reconnected.** Gating
152+ reconnects on ` everReady ` is what stops a connect that fails on
153+ authentication from retrying forever behind a caller that already surfaced
154+ the error.
155+
156+ Secrets (key passphrases, SSH passwords) are stored as ` safeStorage ` ciphertext;
157+ where no OS keychain exists nothing is written to disk and the launcher says so.
158+ Each profile gets its own session partition, so two DeepSQL servers never share
159+ cookies. ` .github/workflows/desktop-release.yml ` builds all three platforms on
160+ their native runners. See ` desktop/README.md ` for the full picture.
161+
96162## MCP Server
97163
98164- ` mcp/deepsql-phase1-server.js ` implements a Phase 1 stdio MCP server for internal rollout.
0 commit comments