| 1 | const https = require("https"); |
| 2 | const http = require("http"); |
| 3 | const { |
| 4 | allAssetNames, |
| 5 | allReleaseAssetNames, |
| 6 | checksumManifestUrl, |
| 7 | releaseAssetUrl, |
| 8 | } = require("./artifacts"); |
| 9 | |
| 10 | const pkg = require("../package.json"); |
| 11 | |
| 12 | function resolveBinaryVersion() { |
| 13 | const configuredVersion = |
| 14 | process.env.DEEPSEEK_TUI_VERSION || |
| 15 | process.env.DEEPSEEK_VERSION || |
| 16 | pkg.deepseekBinaryVersion || |
| 17 | pkg.version; |
| 18 | return String(configuredVersion).trim(); |
| 19 | } |
| 20 | |
| 21 | function resolveRepo() { |
| 22 | return process.env.DEEPSEEK_TUI_GITHUB_REPO || process.env.DEEPSEEK_GITHUB_REPO || "Hmbown/DeepSeek-TUI"; |
| 23 | } |
| 24 | |
| 25 | function requestStatus(url, method = "HEAD", redirects = 0) { |
| 26 | if (redirects > 10) { |
| 27 | throw new Error(`Too many redirects while checking ${url}`); |
| 28 | } |
| 29 | const client = url.startsWith("https:") ? https : http; |
| 30 | return new Promise((resolve, reject) => { |
| 31 | const req = client.request( |
| 32 | url, |
| 33 | { |
| 34 | method, |
| 35 | headers: { |
| 36 | "User-Agent": "deepseek-tui-npm-release-check", |
| 37 | }, |
| 38 | }, |
| 39 | (res) => { |
| 40 | const status = res.statusCode || 0; |
| 41 | const location = res.headers.location; |
| 42 | res.resume(); |
| 43 | if (status >= 300 && status < 400 && location) { |
| 44 | const next = new URL(location, url).toString(); |
| 45 | resolve(requestStatus(next, method, redirects + 1)); |
| 46 | return; |
| 47 | } |
| 48 | resolve(status); |
| 49 | }, |
| 50 | ); |
| 51 | req.on("error", reject); |
| 52 | req.end(); |
| 53 | }); |
| 54 | } |
| 55 | |
| 56 | async function verifyAsset(url, label) { |
| 57 | let status = await requestStatus(url, "HEAD"); |
| 58 | if (status === 403 || status === 405) { |
| 59 | status = await requestStatus(url, "GET"); |
| 60 | } |
| 61 | if (status < 200 || status >= 400) { |
| 62 | throw new Error(`${label} returned HTTP ${status} (${url})`); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | async function downloadText(url) { |
| 67 | const client = url.startsWith("https:") ? https : http; |
| 68 | return new Promise((resolve, reject) => { |
| 69 | client |
| 70 | .get( |
| 71 | url, |
| 72 | { |
| 73 | headers: { |
| 74 | "User-Agent": "deepseek-tui-npm-release-check", |
| 75 | }, |
| 76 | }, |
| 77 | (res) => { |
| 78 | const status = res.statusCode || 0; |
| 79 | if (status >= 300 && status < 400 && res.headers.location) { |
| 80 | const next = new URL(res.headers.location, url).toString(); |
| 81 | resolve(downloadText(next)); |
| 82 | return; |
| 83 | } |
| 84 | if (status !== 200) { |
| 85 | reject(new Error(`Request failed with status ${status}: ${url}`)); |
| 86 | res.resume(); |
| 87 | return; |
| 88 | } |
| 89 | const chunks = []; |
| 90 | res.setEncoding("utf8"); |
| 91 | res.on("data", (chunk) => chunks.push(chunk)); |
| 92 | res.on("end", () => resolve(chunks.join(""))); |
| 93 | }, |
| 94 | ) |
| 95 | .on("error", reject); |
| 96 | }); |
| 97 | } |
| 98 | |
| 99 | function parseChecksumManifest(text) { |
| 100 | const checksums = new Map(); |
| 101 | for (const line of text.split(/\r?\n/)) { |
| 102 | const trimmed = line.trim(); |
| 103 | if (!trimmed) { |
| 104 | continue; |
| 105 | } |
| 106 | const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/); |
| 107 | if (!match) { |
| 108 | throw new Error(`Invalid checksum manifest line: ${trimmed}`); |
| 109 | } |
| 110 | checksums.set(match[2], match[1].toLowerCase()); |
| 111 | } |
| 112 | return checksums; |
| 113 | } |
| 114 | |
| 115 | async function run() { |
| 116 | const version = resolveBinaryVersion(); |
| 117 | const repo = resolveRepo(); |
| 118 | const assets = allReleaseAssetNames(); |
| 119 | |
| 120 | console.log(`Verifying ${assets.length} release assets for ${repo}@v${version}...`); |
| 121 | for (const asset of assets) { |
| 122 | const url = releaseAssetUrl(asset, version, repo); |
| 123 | await verifyAsset(url, asset); |
| 124 | console.log(` ok ${asset}`); |
| 125 | } |
| 126 | const checksums = parseChecksumManifest( |
| 127 | await downloadText(checksumManifestUrl(version, repo)), |
| 128 | ); |
| 129 | for (const asset of allAssetNames()) { |
| 130 | if (!checksums.has(asset)) { |
| 131 | throw new Error(`Checksum manifest is missing ${asset}`); |
| 132 | } |
| 133 | } |
| 134 | console.log("Release assets verified."); |
| 135 | } |
| 136 | |
| 137 | run().catch((error) => { |
| 138 | console.error("Release asset verification failed:", error.message); |
| 139 | process.exit(1); |
| 140 | }); |
| 141 |