feat(install): one-command device registration for macOS, Windows and Linux

Registering a machine meant reading a multi-step page, installing Node, and
running three commands in the right order. Now it is one line per platform.

The hub serves scripts/install.sh and scripts/install.ps1 with its own address
and the enrolment token substituted in, so the published command carries
everything and there is nothing to fill in:

  curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh
  irm https://support.freakma.com/install.ps1?token=TOKEN | iex

That output is piped straight into a shell, so the token — the only untrusted
value in either file — is refused unless it matches the base64url shape that
randomToken produces.

Each script checks for a usable runtime and stops with instructions rather than
guessing, warns when nothing is serving RFB on the loopback, installs per-user
with no root or administrator, and registers a login-scoped service: launchd on
macOS, a lingering systemd user service on Linux, a logon task on Windows. The
Windows script uses Node 22 when it is present and falls back to the bundled
executable otherwise, which is what lets one command cover both Windows 11 and
Server 2008 R2.

It registers a logon task rather than a service on purpose: services run in
session 0 and cannot draw on the interactive desktop, so the "ask first" consent
prompt would never appear.

Also adds /docs — a per-OS setup guide with service management and a
troubleshooting table — and reworks the enrolment page into OS tabs that open on
whichever platform the reader is sitting at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 11:37:12 -07:00
co-authored by Claude Opus 5
parent cecaf74a0a
commit cfda6b19b7
11 changed files with 838 additions and 40 deletions
+36
View File
@@ -82,10 +82,46 @@ app.get('/download/agent.exe', (_req, res) => {
res.download(AGENT_EXE, 'rcs-agent.exe');
});
/* ------------------------------------------------------ install scripts */
// One-command registration: the hub stamps its own address and the enrolment
// token into the script before serving it, so the whole thing is a single
// copy-paste with nothing to fill in.
//
// curl -fsSL https://host/install.sh?token=TOKEN | sh
// irm https://host/install.ps1?token=TOKEN | iex
const SCRIPTS_DIR = path.join(__dirname, '..', 'scripts');
// Tokens are base64url (see randomToken). This output is piped straight into a
// shell, so anything that is not shaped like a token is refused rather than
// interpolated — a token is the only untrusted value in these files.
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
function serveInstallScript(file, contentType) {
return (req, res) => {
const token = String(req.query.token || '');
if (token && !TOKEN_PATTERN.test(token)) {
return res.status(400).type('text/plain').send('that is not a valid enrolment token');
}
let body;
try {
body = fs.readFileSync(path.join(SCRIPTS_DIR, file), 'utf8');
} catch {
return res.status(500).type('text/plain').send('install script missing from this deployment');
}
body = body.split('__HUB__').join(config.baseUrl(req)).split('__TOKEN__').join(token);
res.type(contentType).send(body);
};
}
app.get('/install.sh', serveInstallScript('install.sh', 'text/x-shellscript; charset=utf-8'));
app.get('/install.ps1', serveInstallScript('install.ps1', 'text/plain; charset=utf-8'));
app.use('/novnc', express.static(NOVNC_DIR, { maxAge: '7d', immutable: true }));
app.use(express.static(PUBLIC_DIR));
app.get('/viewer', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'viewer.html')));
app.get('/docs', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'docs.html')));
app.get('/enroll/:token', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'enroll.html')));
app.get('/s/:token', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'share.html')));