mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-25 02:03:03 +00:00
Add permissions check, shasum check, & build info
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user