Add permissions check, shasum check, & build info

This commit is contained in:
Owen
2026-05-21 14:34:16 -07:00
parent ed73d089d0
commit dee0ca6864

View File

@@ -13,23 +13,31 @@ import logger from "@server/logger";
import cache from "#dynamic/lib/cache"; import cache from "#dynamic/lib/cache";
import config from "@server/lib/config"; import config from "@server/lib/config";
// Stale-while-revalidate cache for the latest newt version. // Stale-while-revalidate in-memory fallback for the releases API.
let staleNewtVersion: string | null = null; type ReleaseInfo = {
version: string;
// binary filename -> sha256 hex (sourced from asset `digest` field in GitHub API)
assetDigests: Record<string, string>;
};
let staleReleaseInfo: ReleaseInfo | null = null;
async function getLatestNewtVersion(): Promise<string | null> { /**
* Fetches the latest stable newt release from GitHub and returns the version
* tag together with a map of asset-name → sha256 hex digest.
* Results are cached for one hour; stale data is returned on failure.
*/
async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
try { try {
const cachedVersion = await cache.get<string>( const cached = await cache.get<ReleaseInfo>("cache:newtReleaseInfo");
"cache:latestNewtVersion" if (cached) {
); return cached;
if (cachedVersion) {
return cachedVersion;
} }
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500); const timeoutId = setTimeout(() => controller.abort(), 5000);
const fetchResponse = await fetch( const fetchResponse = await fetch(
"https://api.github.com/repos/fosrl/newt/tags", "https://api.github.com/repos/fosrl/newt/releases",
{ signal: controller.signal } { signal: controller.signal }
); );
@@ -37,57 +45,71 @@ async function getLatestNewtVersion(): Promise<string | null> {
if (!fetchResponse.ok) { if (!fetchResponse.ok) {
logger.warn( logger.warn(
`Failed to fetch latest Newt version from GitHub: ${fetchResponse.status} ${fetchResponse.statusText}` `Failed to fetch Newt releases from GitHub: ${fetchResponse.status} ${fetchResponse.statusText}`
); );
return staleNewtVersion; return staleReleaseInfo;
} }
let tags = await fetchResponse.json(); let releases: any[] = await fetchResponse.json();
if (!Array.isArray(tags) || tags.length === 0) { if (!Array.isArray(releases) || releases.length === 0) {
logger.warn("No tags found for Newt repository"); logger.warn("No releases found for Newt repository");
return staleNewtVersion; return staleReleaseInfo;
} }
tags = tags.filter((tag: any) => !tag.name.includes("rc")); // Drop drafts, pre-releases, and anything with "rc" in the tag name.
tags.sort((a: any, b: any) => { releases = releases.filter(
const va = semver.coerce(a.name); (r: any) => !r.draft && !r.prerelease && !r.tag_name.includes("rc")
const vb = semver.coerce(b.name); );
// Sort descending by semver to find the true latest stable release.
releases.sort((a: any, b: any) => {
const va = semver.coerce(a.tag_name);
const vb = semver.coerce(b.tag_name);
if (!va && !vb) return 0; if (!va && !vb) return 0;
if (!va) return 1; if (!va) return 1;
if (!vb) return -1; if (!vb) return -1;
return semver.rcompare(va, vb); return semver.rcompare(va, vb);
}); });
const seen = new Set<string>(); if (releases.length === 0) {
tags = tags.filter((tag: any) => { logger.warn("No stable releases found for Newt repository");
const normalised = semver.coerce(tag.name)?.version; return staleReleaseInfo;
if (!normalised || seen.has(normalised)) return false;
seen.add(normalised);
return true;
});
if (tags.length === 0) {
logger.warn("No valid semver tags found for Newt repository");
return staleNewtVersion;
} }
const latestVersion = tags[0].name; const latest = releases[0];
staleNewtVersion = latestVersion; const version: string = latest.tag_name;
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
return latestVersion; // Build a map of binary filename → sha256 hex from the asset `digest`
// field returned by the GitHub API (format: "sha256:<hex>").
const assetDigests: Record<string, string> = {};
if (Array.isArray(latest.assets)) {
for (const asset of latest.assets) {
if (
typeof asset.name === "string" &&
typeof asset.digest === "string" &&
asset.digest.startsWith("sha256:")
) {
assetDigests[asset.name] = asset.digest.slice(
"sha256:".length
);
}
}
}
const info: ReleaseInfo = { version, assetDigests };
staleReleaseInfo = info;
await cache.set("cache:newtReleaseInfo", info, 3600);
return info;
} catch (error: any) { } catch (error: any) {
if (error.name === "AbortError") { if (error.name === "AbortError") {
logger.warn( logger.warn("Request to fetch Newt releases timed out (5s)");
"Request to fetch latest Newt version timed out (1.5s)"
);
} else { } else {
logger.warn( logger.warn(
"Error fetching latest Newt version:", "Error fetching Newt releases:",
error.message || error error.message || error
); );
} }
return staleNewtVersion; return staleReleaseInfo;
} }
} }
@@ -103,6 +125,7 @@ export type GetNewtVersionResponse = {
latestVersion: string; latestVersion: string;
currentIsLatest: boolean; currentIsLatest: boolean;
downloadUrl: string; downloadUrl: string;
sha256: string;
}; };
export async function getNewtVersion( export async function getNewtVersion(
@@ -137,10 +160,7 @@ export async function getNewtVersion(
); );
} }
return next( return next(
createHttpError( createHttpError(HttpCode.UNAUTHORIZED, "Invalid credentials")
HttpCode.UNAUTHORIZED,
"Invalid credentials"
)
); );
} }
@@ -157,17 +177,14 @@ export async function getNewtVersion(
); );
} }
return next( return next(
createHttpError( createHttpError(HttpCode.UNAUTHORIZED, "Invalid credentials")
HttpCode.UNAUTHORIZED,
"Invalid credentials"
)
); );
} }
// Fetch latest version // Fetch latest release info (version + asset digests) in one API call.
const latestVersion = await getLatestNewtVersion(); const releaseInfo = await getLatestReleaseInfo();
if (!latestVersion) { if (!releaseInfo) {
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
@@ -176,19 +193,24 @@ export async function getNewtVersion(
); );
} }
// Normalise the tag (strip leading 'v' for the URL, but keep original for comparison) const latestVersion = releaseInfo.version;
// Normalise the tag (ensure leading 'v') for the download URL.
const tagForUrl = latestVersion.startsWith("v") const tagForUrl = latestVersion.startsWith("v")
? latestVersion ? latestVersion
: `v${latestVersion}`; : `v${latestVersion}`;
// Binary name follows the get-newt.sh convention: newt_<platform>[.exe] // Binary name follows the get-newt.sh convention: newt_<platform>[.exe]
const binaryName = const binaryName = platform.includes("windows")
platform.includes("windows")
? `newt_${platform}.exe` ? `newt_${platform}.exe`
: `newt_${platform}`; : `newt_${platform}`;
const downloadUrl = `https://github.com/fosrl/newt/releases/download/${tagForUrl}/${binaryName}`; const downloadUrl = `https://github.com/fosrl/newt/releases/download/${tagForUrl}/${binaryName}`;
// Look up the SHA256 digest for this specific binary from the GitHub
// release asset metadata (the `digest` field, format "sha256:<hex>").
const sha256 = releaseInfo.assetDigests[binaryName] ?? "";
// Determine whether the newt that's asking is already up to date. // Determine whether the newt that's asking is already up to date.
// We store the current version on the newt row when it registers. // We store the current version on the newt row when it registers.
const currentVersion = existingNewt.version ?? null; const currentVersion = existingNewt.version ?? null;
@@ -209,7 +231,8 @@ export async function getNewtVersion(
data: { data: {
latestVersion, latestVersion,
currentIsLatest, currentIsLatest,
downloadUrl downloadUrl,
sha256
}, },
success: true, success: true,
error: false, error: false,