feat(install): the desktop app downloads from a link that never changes (#394)

`/download/windows` and `/download/linux` redirect to a one-minute
signed URL for the current desktop app (`desktop/<version>/` in the
releases bucket). `/desktop.sh` installs it on Linux for the calling
user, without sudo. The version handed out is a constant bumped by a
commit after each release. Tests: 180 pass.
This commit is contained in:
Wanjohi
2026-10-11 12:04:49 +00:00
committed by GitHub
4 changed files with 133 additions and 19 deletions
+69 -19
View File
@@ -5,6 +5,8 @@ import { describeRoute } from 'hono-openapi';
// The installer, embedded at build time so the script and the API that redeems
// its token always ship as one version.
import script from '../../install/install.sh' with { type: 'text' };
import desktopScript from '../../install/desktop.sh' with { type: 'text' };
import { DESKTOP_VERSION } from '../../install/desktop-version';
import { presignGet } from '../utils/presign';
/**
@@ -16,12 +18,37 @@ import { presignGet } from '../utils/presign';
*/
export namespace InstallApi {
/** What may be downloaded: one component, versions and asset names by shape. */
const COMPONENTS = new Set(['host']);
const COMPONENTS = new Set(['host', 'desktop']);
const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/;
const ASSET = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const SIGNED_SECONDS = 60;
/** The desktop app's asset for each platform a person can download it for. */
const DESKTOP_ASSETS: Record<string, string> = {
windows: 'Nestri.exe',
linux: 'nestri-linux-x86_64'
};
function releases() {
const env = Env.get();
if (
!env.RELEASES_BUCKET ||
!env.RELEASES_ENDPOINT ||
!env.RELEASES_ACCESS_KEY_ID ||
!env.RELEASES_SECRET_ACCESS_KEY
) {
return null;
}
return {
endpoint: env.RELEASES_ENDPOINT,
bucket: env.RELEASES_BUCKET,
region: env.RELEASES_REGION,
accessKeyId: env.RELEASES_ACCESS_KEY_ID,
secretAccessKey: env.RELEASES_SECRET_ACCESS_KEY
};
}
export const route = new Hono()
.get(
'/install.sh',
@@ -55,26 +82,49 @@ export namespace InstallApi {
if (!COMPONENTS.has(component) || !VERSION.test(version) || !ASSET.test(asset)) {
return c.notFound();
}
const env = Env.get();
if (
!env.RELEASES_BUCKET ||
!env.RELEASES_ENDPOINT ||
!env.RELEASES_ACCESS_KEY_ID ||
!env.RELEASES_SECRET_ACCESS_KEY
) {
const bucket = releases();
if (!bucket) {
return c.json({ message: 'Downloads are not configured on this deployment.' }, 503);
}
const url = await presignGet(
{
endpoint: env.RELEASES_ENDPOINT,
bucket: env.RELEASES_BUCKET,
region: env.RELEASES_REGION,
accessKeyId: env.RELEASES_ACCESS_KEY_ID,
secretAccessKey: env.RELEASES_SECRET_ACCESS_KEY
},
`${component}/${version}/${asset}`,
SIGNED_SECONDS
);
const url = await presignGet(bucket, `${component}/${version}/${asset}`, SIGNED_SECONDS);
return c.redirect(url, 302);
}
)
.get(
'/desktop.sh',
describeRoute({
tags: ['Install'],
summary: 'The desktop app installer for Linux',
description:
'A POSIX shell script that installs the desktop app for the calling user, in ~/.local/bin with a launcher entry. Pipe it to `sh`.',
responses: { 200: { description: 'The script' } }
}),
(c) =>
c.body(desktopScript, 200, {
'content-type': 'text/x-shellscript; charset=utf-8',
'cache-control': 'no-cache'
})
)
.get(
'/download/:platform',
describeRoute({
tags: ['Install'],
summary: 'Download the desktop app',
description:
'Redirects to a short-lived signed URL for the current desktop app on `windows` or `linux`. This link does not change between versions.',
responses: {
302: { description: 'Where to download it' },
404: { description: 'Not available for that platform' }
}
}),
async (c) => {
const asset = DESKTOP_ASSETS[c.req.param('platform')];
if (!asset) return c.notFound();
const bucket = releases();
if (!bucket) {
return c.json({ message: 'Downloads are not configured on this deployment.' }, 503);
}
const url = await presignGet(bucket, `desktop/${DESKTOP_VERSION}/${asset}`, SIGNED_SECONDS);
return c.redirect(url, 302);
}
);
+6
View File
@@ -0,0 +1,6 @@
/**
* The desktop app every download link hands out. Bumped by a commit once a
* release of that version is published, the same way the host installer's
* default is, so what a person downloads changes only through review.
*/
export const DESKTOP_VERSION = '0.1.0';
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
# Installs the Nestri desktop app for the calling user: one binary in
# ~/.local/bin and a launcher entry. No root, nothing outside $HOME.
#
# curl -fsSL https://api.nestri.io/desktop.sh | sh
set -eu
API="${NESTRI_API:-https://api.nestri.io}"
BIN="${XDG_BIN_HOME:-$HOME/.local/bin}"
APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) ;;
*) echo "Nestri runs on x86_64 Linux and Windows for now." >&2; exit 1 ;;
esac
mkdir -p "$BIN" "$APPS"
tmp="$BIN/.nestri.$$"
trap 'rm -f "$tmp"' EXIT
echo "Downloading Nestri..."
curl -fSL --progress-bar "$API/download/linux" -o "$tmp"
chmod 755 "$tmp"
mv "$tmp" "$BIN/nestri"
cat > "$APPS/nestri.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=Nestri
Comment=Play your games on our GPUs
Exec=$BIN/nestri
Terminal=false
Categories=Game;
DESKTOP
echo "Installed. Open Nestri from your app launcher, or run: $BIN/nestri"
case ":$PATH:" in *":$BIN:"*) ;; *) echo "($BIN is not on your PATH.)" ;; esac
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, test } from 'bun:test';
import { app } from '../app/index';
describe('the desktop app download', () => {
test('the Linux installer is served as a script', async () => {
const res = await app.request('/desktop.sh');
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toContain('shellscript');
expect(await res.text()).toContain('/download/linux');
});
test('a platform that is not offered is not found', async () => {
expect((await app.request('/download/macos')).status).toBe(404);
});
test('a desktop asset is a known component', async () => {
// No bucket in tests: the route got past its allow-list to the config check.
const res = await app.request('/install/desktop/0.1.0/Nestri.exe');
expect([302, 503]).toContain(res.status);
});
});