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