feat(desktop): downloads from a configured public GitHub releases repo

Merged
KH
1h ago
+649-1

Why

The dashboard's Desktop downloads card (now on Settings > Workspace) reads only the signed Desktop feed. That feed needs AWS KMS and S3 in production, so a local Kind stack shows "There are no Desktop downloads". The Kind Desktop is published to a public GitHub releases repo and updates from it, so the card can show that instead.

What

SELFHOST_DESKTOP_RELEASES_REPO=<owner>/<repo> (strict ^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$, unset by default) turns on GET /api/v1/desktop/releases for any signed-in member. It returns { source: { kind: "github", repo, releasesUrl }, release }, where release is the newest non-draft release (pre-releases included) in the card's manifest shape. Installer names Desktop-<profile>-<version>-<OS>-<arch>[-probe|-Setup].<ext> map to platform, arch, kind and the macOS os_floor; anything else is skipped.

Bounds

  • Control calls only https://api.github.com/repos/<owner>/<repo>/releases: the release list, and each .sha256 sidecar through its API asset URL. That URL's single redirect may only go to *.githubusercontent.com.
  • No token. The list is capped at 2 MiB, a sidecar at 1 KiB, and the whole read at 8 s.
  • Results are cached for 5 minutes, failures included. Any upstream failure returns release: null, never a 5xx.

Integrity

  • An asset is served only with a matching sidecar from the same release (digest, plus the file name when present).
  • If GitHub reports its own sha256: digest for the asset, it must agree with the sidecar.
  • This gives integrity, not publisher authenticity. The webapp labels the source as GitHub releases, not the signed Desktop feed, and uses it only when the signed feed has no profiles.

URLs: url is kept only if it's https://github.com or https://objects.githubusercontent.com under /<owner>/<repo>/releases/download/.

Unset: 404, so nothing changes for any other install. Kind sets it in its own overlay.

Tests

  • test/desktop-releases.test.ts:
    • the repo setting's format check;
    • name mapping for every real Kind asset name, with non-installers skipped;
    • the URL allow-list (other hosts, repos and paths, ports, credentials and query strings are refused);
    • mapping with sidecar checksums: newest non-draft release, pre-release kept, an asset without a sidecar dropped;
    • the 5-minute cache;
    • only api.github.com and githubusercontent hosts are called;
    • assets dropped for: digest mismatch, sidecar naming a different file, foreign download URL, redirect to another host (which is never fetched);
    • upstream 503, a thrown fetch and an empty list all give release: null, with the failure cached for 5 minutes.
  • test/sessions.test.ts: 404 when unset, 401 without a token, 200 for a member with no-store, and exactly one GitHub call.

npm test passes locally (1226). Companion webapp PR: relay-webapp branch feat/downloads-github-releases.

๐Ÿค– Generated with Claude Code


Autofix with [code]smith
Open in [code]smith ยท Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds GET /api/v1/desktop/releases so the downloads card can show the latest release from a public GitHub repo when the signed Desktop feed has nothing, such as on a local Kind stack. It's gated by a new SELFHOST_DESKTOP_RELEASES_REPO setting, unset by default, in which case the route answers 404 and nothing changes.

Security and integrity bounds

  • Calls only api.github.com (release list plus each .sha256 sidecar's asset URL), with one redirect allowed only to *.githubusercontent.com; no token, size caps, 8-second deadline, and a 5-minute cache that includes failures.
  • An asset is served only with a matching sidecar from the same release whose digest agrees with GitHub's own when reported; download URLs are kept only on github.com and objects.githubusercontent.com under /<owner>/<repo>/releases/download/, with the repo compared case-insensitively.
  • Refreshes stay inside GitHub's unauthenticated quota: the list is a conditional request (a 304 costs nothing) and verified sidecar digests are cached per asset, so unchanged assets are never re-read.
  • Any upstream failure serves the last complete release, falling back to release: null only when none exists โ€” never a 5xx โ€” and the dashboard labels the source as GitHub releases rather than the signed feed.