export interface UpdateManifest { guideUrl: string; latestVersion: string; minSupportedVersion: string; publishedAt: string; releaseNotes: string[]; zipUrl: string; } export function compareExtensionVersions(left: string, right: string): number { const leftParts = parseVersionParts(left); const rightParts = parseVersionParts(right); const maxLength = Math.max(leftParts.length, rightParts.length); for (let index = 0; index < maxLength; index += 1) { const leftValue = leftParts[index] ?? 0; const rightValue = rightParts[index] ?? 0; if (leftValue !== rightValue) { return leftValue - rightValue; } } return 0; } export function parseUpdateManifest(value: unknown): UpdateManifest | null { if (!value || typeof value !== "object") { return null; } const candidate = value as Partial; if ( !isVersionString(candidate.latestVersion) || !isVersionString(candidate.minSupportedVersion) || !isHttpsUrl(candidate.zipUrl) || !isHttpsUrl(candidate.guideUrl) || typeof candidate.publishedAt !== "string" || !Array.isArray(candidate.releaseNotes) || !candidate.releaseNotes.every((note) => typeof note === "string") ) { return null; } return { guideUrl: candidate.guideUrl, latestVersion: candidate.latestVersion, minSupportedVersion: candidate.minSupportedVersion, publishedAt: candidate.publishedAt, releaseNotes: candidate.releaseNotes, zipUrl: candidate.zipUrl }; } export async function fetchUpdateManifest( manifestUrl: string, fetchImpl: typeof fetch = fetch ): Promise { const response = await fetchImpl(manifestUrl, { cache: "no-store" }); if (!response.ok) { throw new Error(`update manifest request failed: ${response.status}`); } const manifest = parseUpdateManifest(await response.json()); if (!manifest) { throw new Error("update manifest is invalid"); } return manifest; } function parseVersionParts(value: string): number[] { return value.split(".").map((part) => { const parsed = Number.parseInt(part, 10); return Number.isFinite(parsed) ? parsed : 0; }); } function isVersionString(value: unknown): value is string { return typeof value === "string" && /^\d+(?:\.\d+)*$/.test(value); } function isHttpsUrl(value: unknown): value is string { if (typeof value !== "string") { return false; } try { return new URL(value).protocol === "https:"; } catch { return false; } }