Skip to content

Commit f4a43cb

Browse files
K. Kaushik ReddyK. Kaushik Reddy
authored andcommitted
desktop-client init
1 parent ced34dc commit f4a43cb

36 files changed

Lines changed: 9182 additions & 0 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: desktop-release
2+
3+
# Builds the DeepSQL desktop client for every platform. Each OS builds its own
4+
# targets on its own runner: cross-building Windows needs Wine and Linux targets
5+
# need a matching glibc, and both are far less reliable than just using the
6+
# native runner.
7+
#
8+
# Tag-triggered runs attach the installers to the GitHub release. Manual runs
9+
# upload them as workflow artifacts, which is the easy way to hand a build to a
10+
# colleague before there is a release to cut.
11+
12+
on:
13+
push:
14+
tags:
15+
- 'desktop-v*'
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
build:
23+
name: ${{ matrix.name }}
24+
runs-on: ${{ matrix.os }}
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
include:
29+
- { os: macos-latest, name: macOS, target: '--mac' }
30+
- { os: windows-latest, name: Windows, target: '--win' }
31+
- { os: ubuntu-latest, name: Linux, target: '--linux' }
32+
33+
steps:
34+
- uses: actions/checkout@v7
35+
36+
- uses: actions/setup-node@v7
37+
with:
38+
node-version: 22
39+
cache: npm
40+
cache-dependency-path: desktop/package-lock.json
41+
42+
- name: Install dependencies
43+
working-directory: desktop
44+
run: npm ci
45+
46+
- name: Self-test the SSH tunnel transport
47+
# Runs a real SSH server in-process, so it needs a display-free Electron.
48+
# xvfb is only required on Linux; the other runners have a window server.
49+
working-directory: desktop
50+
run: ${{ matrix.os == 'ubuntu-latest' && 'xvfb-run --auto-servernum npm run selftest:tunnel' || 'npm run selftest:tunnel' }}
51+
shell: bash
52+
53+
- name: Build installers
54+
working-directory: desktop
55+
env:
56+
# Signing is skipped when these are unset; electron-builder warns and
57+
# produces unsigned artifacts rather than failing the build.
58+
CSC_LINK: ${{ secrets.DESKTOP_CSC_LINK }}
59+
CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_CSC_KEY_PASSWORD }}
60+
APPLE_ID: ${{ secrets.APPLE_ID }}
61+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
62+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
63+
run: npx electron-builder ${{ matrix.target }} --publish never
64+
65+
- uses: actions/upload-artifact@v4
66+
with:
67+
name: deepsql-desktop-${{ matrix.name }}
68+
if-no-files-found: error
69+
path: |
70+
desktop/release/*.dmg
71+
desktop/release/*.zip
72+
desktop/release/*.exe
73+
desktop/release/*.AppImage
74+
desktop/release/*.deb
75+
desktop/release/*.rpm
76+
77+
release:
78+
name: attach to release
79+
needs: build
80+
if: startsWith(github.ref, 'refs/tags/desktop-v')
81+
runs-on: ubuntu-latest
82+
permissions:
83+
contents: write
84+
steps:
85+
- uses: actions/download-artifact@v4
86+
with:
87+
path: artifacts
88+
merge-multiple: true
89+
90+
- uses: softprops/action-gh-release@v2
91+
with:
92+
files: artifacts/*
93+
fail_on_unmatched_files: true

AGENTS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ User Message → ChatController → SpringAIChatService
110110
- **UI state**: Zustand stores with selector hooks (`useActiveTab`, `useDashboardActions`)
111111
- **Independent chat threads**: Per-tab, per-connection, stored in localStorage
112112

113+
## Desktop Client (Electron)
114+
115+
`desktop/` is a standalone Electron app (its own `package.json`, not part of the
116+
root npm project). It is a **thin client**: it never bundles the React frontend,
117+
it navigates a `WebContentsView` at the real DeepSQL origin, so the UI is always
118+
the version the server runs. Two transports resolve to that origin — direct TLS,
119+
or an in-process SSH local forward (`ssh2`, no `ssh` binary needed).
120+
121+
| Path | Purpose |
122+
|------|---------|
123+
| `desktop/src/main/transport.js` | Transport manager: connect/disconnect/health per profile |
124+
| `desktop/src/main/tunnel.js` | SSH local forward, host-key TOFU-then-strict, auto-reconnect |
125+
| `desktop/src/main/tls.js` | Cert policy (system / pinned / custom CA / TOFU) for Node **and** Chromium |
126+
| `desktop/src/main/profiles.js` | Connection profiles; secrets only as `safeStorage` ciphertext |
127+
| `desktop/src/main/windows/workspace.js` | Frameless shell: native chrome + embedded DeepSQL view |
128+
| `desktop/src/renderer/shared/theme.css` | Mirrors `src/index.css` tokens — keep in step |
129+
130+
No backend change was needed: `docker/nginx/default.conf` already serves the SPA,
131+
`/api` and `/agent-api` from one origin, which is what makes the thin-client
132+
model work without CORS or cookie special-casing.
133+
113134
## Performance & Safety Guardrails
114135

115136
- Log size cap (500MB) via stream wrappers

CLAUDE.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,72 @@ mcp/ # DeepSQL Phase 1 MCP server (Node stdio wrapper around back
122122
agent/ # DeepSQL Agent (persona, skills, skins, Dockerfile for the Compose service)
123123
```
124124

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

127193
- `mcp/deepsql-phase1-server.js` implements a Phase 1 stdio MCP server for internal rollout.

desktop/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
release/
3+
build/icon.iconset/
4+
*.log

0 commit comments

Comments
 (0)