Files
cinny/src/app/utils/updateErrors.ts
T
Lotus CIandClaude Opus 5.5 674aef94fb
CI / Build & Quality Checks (pull_request) Successful in 1m54s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Failing after 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 12m47s
fix(desktop): the Linux update command verifies the signed SHA256SUMS before installing (cinny-desktop #36)
The pacman / apt update command installed the downloaded package with
no integrity check. Download it with the release's SHA256SUMS.txt and
.minisig into a fresh temp dir, verify the signature with the updater
key (minisign), check the hash, then install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-19 00:37:34 -04:00

108 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Which step of the desktop update failed (the native side prefixes its errors). */
export type UpdatePhase = 'check' | 'download' | 'install';
/** Where to get the installer by hand when the in-app update keeps failing. */
export const MANUAL_DOWNLOAD_URL = {
windows:
'https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/LotusChat-x86_64-setup.exe',
other: 'https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/tag/latest',
};
export const manualDownloadUrl = (userAgent: string): string =>
/Windows/i.test(userAgent) ? MANUAL_DOWNLOAD_URL.windows : MANUAL_DOWNLOAD_URL.other;
const NETWORK_PATTERN =
/error sending request|timed out|timeout|connection|connect|dns|network|reset|refused|unreachable|Could not fetch a valid release/i;
export const isNetworkUpdateError = (message: string): boolean => NETWORK_PATTERN.test(message);
/** Split `download: error sending request …` into its phase and the raw message. */
export const parseUpdateError = (
raw: string,
fallback: UpdatePhase,
): { phase: UpdatePhase; message: string } => {
const match = /^(check|download|install): ([\s\S]*)$/.exec(raw);
return match
? { phase: match[1] as UpdatePhase, message: match[2] }
: { phase: fallback, message: raw };
};
/**
* A plain-language sentence for an update failure. The raw error ("error
* sending request for url (…nsis.zip)") told users nothing about what to do.
*/
export const describeUpdateError = (phase: UpdatePhase, message: string): string => {
const network = isNetworkUpdateError(message);
if (phase === 'check') {
return network
? 'Couldn’t reach the update server. It may be busy, or your connection dropped. Lotus Chat still works; try again in a minute.'
: 'Couldn’t check for updates. Lotus Chat still works; try again later.';
}
if (phase === 'download') {
return network
? 'The update didn’t finish downloading after several tries. The update server may be busy. Try again in a minute, or download the installer yourself.'
: /signature|minisign|base64/i.test(message)
? 'The downloaded update didn’t pass its safety check, so it wasn’t installed. Try again, or download the installer yourself.'
: 'The update couldn’t be downloaded. Try again, or download the installer yourself.';
}
return 'The update downloaded but couldn’t be installed. Download the installer yourself and run it; your chats and settings are kept.';
};
/**
* How this desktop install gets updated (cinny-desktop `update_install_kind`).
* Linux package installs can't be replaced in place by the in-app updater
* (it tried to write into /usr/bin: "Permission denied (os error 13)"), so
* they get their package manager's command instead of an Install button.
*/
export type InstallKind = 'in-app' | 'pacman' | 'deb' | 'manual';
const RELEASE_DOWNLOAD =
'https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest';
export const toInstallKind = (value: unknown): InstallKind =>
value === 'pacman' || value === 'deb' || value === 'manual' ? value : 'in-app';
export type PackageUpdateHelp = { command?: string; url: string; download: string };
/** The desktop updater's minisign public key (cinny-desktop tauri.conf.json). */
export const RELEASE_MINISIGN_KEY = 'RWRZZdyii05/NUcz337A5SAbiZK+NPVDWuiL2mM5Jk1p/LfRmNfiZ/6l';
/**
* [cinny-desktop #36] Download a Linux package and install it only after
* checking the release's minisign-signed SHA256SUMS.txt. Runs in a fresh temp
* directory so a stale SHA256SUMS.txt can't be picked up.
*/
const verifiedInstall = (file: string, install: string): string =>
[
`cd "$(mktemp -d)"`,
`curl -fLO ${RELEASE_DOWNLOAD}/${file} -fLO ${RELEASE_DOWNLOAD}/SHA256SUMS.txt -fLO ${RELEASE_DOWNLOAD}/SHA256SUMS.txt.minisig`,
`minisign -Vm SHA256SUMS.txt -P ${RELEASE_MINISIGN_KEY}`,
`sha256sum -c --ignore-missing SHA256SUMS.txt`,
`${install} ./${file}`,
].join(' && ');
export const packageUpdateHelp = (kind: InstallKind): PackageUpdateHelp | undefined => {
if (kind === 'pacman') {
// Install the local file: `pacman -U <url>` also fetches `<url>.sig` and
// fails without it (packages aren't signed for pacman), while a local file
// falls under LocalFileSigLevel. Integrity comes from SHA256SUMS.txt.
return {
command: verifiedInstall('LotusChat-x86_64.pkg.tar.zst', 'sudo pacman -U'),
url: `${RELEASE_DOWNLOAD}/LotusChat-x86_64.pkg.tar.zst`,
download: 'Download package',
};
}
if (kind === 'deb') {
return {
command: verifiedInstall('LotusChat-x86_64.deb', 'sudo apt install'),
url: `${RELEASE_DOWNLOAD}/LotusChat-x86_64.deb`,
download: 'Download package',
};
}
if (kind === 'manual') return { url: MANUAL_DOWNLOAD_URL.other, download: 'Open downloads' };
return undefined;
};
/** The native side refuses an in-app install on a package install. */
export const isPackageManagedError = (message: string): boolean => /package-managed/.test(message);