{"record":{"id":"f7364ea829ebdc5b","repo":"Hmbown/CodeWhale","slug":"invalid-update-archive","errorCode":null,"errorMessage":"Invalid update archive.","messagePattern":"Invalid update archive\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/app/updates.mjs","lineNumber":53,"sourceCode":"  if(!version||release.draft||release.prerelease||!newerVersion(version,current)) return {available:false,message:`You have Computer Use ${current}. No newer stable installer is available.`};\n  const name=`Codewhale-Computer-Use-${version}-macos-universal.zip`;\n  const asset=release.assets?.find(asset=>asset.name===name);\n  const url=`${repository}/releases/download/v${version}/${name}`;\n  if(!asset||asset.browser_download_url!==url||!/^sha256:[a-f0-9]{64}$/.test(asset.digest)||!Number.isSafeInteger(asset.size)||asset.size<=0||asset.size>limit) return {available:false,message:`Version ${version} has no verified macOS installer yet.`};\n  return {available:true,version,url,sha256:asset.digest.slice(7),size:asset.size,message:`Computer Use ${version} is available. Install it to restart the helper; existing computer sessions will stop.`};\n}\nexport async function checkForUpdate() {\n  const response=await fetch(\"https://api.github.com/repos/Hmbown/codewhale-cu-plugin/releases/latest\",{redirect:\"error\",headers:{Accept:\"application/vnd.github+json\",\"X-GitHub-Api-Version\":\"2022-11-28\"},signal:AbortSignal.timeout(10_000)});\n  if(response.status===404) return {available:false,message:\"No stable installer has been published yet. Your current app is unchanged.\"};\n  if(!response.ok) throw new Error(`The update service is unavailable (${response.status}). Try again later.`);\n  return releaseUpdate(JSON.parse((await responseBytes(response,1024*1024)).toString(\"utf8\")));\n}\n\n/** Inspect both ZIP headers before extraction: no links, traversal or bombs. */\nexport function validateReleaseZip(bytes) {\n  const minimum=Math.max(0,bytes.length-65557); let end=-1;\n  for(let i=bytes.length-22;i>=minimum;i--) if(bytes.readUInt32LE(i)===0x06054b50&&i+22+bytes.readUInt16LE(i+20)===bytes.length) { end=i; break; }\n  if(end<0||bytes.readUInt16LE(end+4)||bytes.readUInt16LE(end+6)) throw new Error(\"Invalid update archive.\");\n  const count=bytes.readUInt16LE(end+10); let position=bytes.readUInt32LE(end+16),total=0;\n  if(!count||count>2000||bytes.readUInt16LE(end+8)!==count||position+bytes.readUInt32LE(end+12)!==end) throw new Error(\"Invalid update archive index.\");\n  const seen=new Set();\n  for(let i=0;i<count;i++) {\n    if(position+46>end||bytes.readUInt32LE(position)!==0x02014b50) throw new Error(\"Invalid update entry.\");\n    const flags=bytes.readUInt16LE(position+8),method=bytes.readUInt16LE(position+10),length=bytes.readUInt16LE(position+28),extra=bytes.readUInt16LE(position+30),comment=bytes.readUInt16LE(position+32);\n    const name=bytes.subarray(position+46,position+46+length).toString(\"utf8\");\n    const kind=(bytes.readUInt32LE(position+38)>>>16)&0xf000,offset=bytes.readUInt32LE(position+42),compressed=bytes.readUInt32LE(position+20);\n    const size=bytes.readUInt32LE(position+24); total+=size;\n    if(flags&1||![0,8].includes(method)||![0,0x4000,0x8000].includes(kind)||total>512*1024*1024||position+46+length+extra+comment>end) throw new Error(\"Unsupported update entry.\");\n    if(!name.startsWith(`${APP_NAME}.app/`)||name.includes(\"\\\\\")||name.includes(\":\")||name.includes(\"\\0\")||name.split(\"/\").some(part=>part===\"..\"||part===\".\")||seen.has(name)) throw new Error(\"Unsafe update path.\");\n    seen.add(name);\n    if(offset+30>position||bytes.readUInt32LE(offset)!==0x04034b50) throw new Error(\"Invalid update file header.\");\n    const localLength=bytes.readUInt16LE(offset+26),localExtra=bytes.readUInt16LE(offset+28);\n    if(offset+30+localLength+localExtra+compressed>bytes.readUInt32LE(end+16)||bytes.subarray(offset+30,offset+30+localLength).toString(\"utf8\")!==name) throw new Error(\"Inconsistent update file header.\");\n    if(bytes.readUInt16LE(offset+8)!==method||bytes.readUInt16LE(offset+6)!==flags||(!(flags&8)&&(bytes.readUInt32LE(offset+18)!==compressed||bytes.readUInt32LE(offset+22)!==size))) throw new Error(\"Inconsistent update sizes or compression.\");\n    const start=offset+30+localLength+localExtra;\n    // Header sizes are untrusted. Bound actual expansion before ditto writes","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/app/updates.mjs#L35-L71","documentation":"validateReleaseZip scans the buffer for the End of Central Directory record (0x06054b50) whose comment length exactly reaches the end of file; it throws 'Invalid update archive.' when no valid EOCD is found or the disk-number fields in the EOCD are non-zero (multi-disk archives are unsupported).","triggerScenarios":"Calling validateReleaseZip on bytes that are not a supported ZIP: truncated download, an HTML error page, multi-disk/segmented archives, an EOCD with non-zero disk numbers, or an EOCD whose comment length does not match the buffer end.","commonSituations":"Download corrupted or interrupted mid-transfer; CDN/proxy returning an error page instead of the asset; an archive produced with ZIP64 or spanning features; checksum not verified before validation.","solutions":["Re-download the release asset and confirm its SHA-256 matches the digest published on the release","Inspect the first bytes (PK\\u0003\\u0004 signature) to confirm the file is actually a ZIP, not an HTML error page","Repackage without ZIP64/spanning options if the archive was built with them","Verify asset size against the release metadata before validation"],"exampleFix":"// before (validate whatever came back)\ncode.validateReleaseZip(bytes);\n// after (verify digest first)\nif (code.createHash(\"sha256\").update(bytes).digest(\"hex\") !== expectedSha) throw new Error(\"download digest mismatch\");\ncode.validateReleaseZip(bytes);","handlingStrategy":"validation","validationCode":"function looksLikeZip(bytes) {\n  return bytes.length >= 22 && bytes.readUInt32LE(0) === 0x04034b50;\n}","typeGuard":null,"tryCatchPattern":"try {\n  validateReleaseZip(bytes);\n} catch (e) {\n  if (e.message === \"Invalid update archive.\") {\n    discardDownload(); // corrupt or hostile; do not extract\n  } else throw e;\n}","preventionTips":["Verify the asset's SHA-256 digest before parsing","Ensure downloads complete fully (check Content-Length vs received bytes)","Avoid ZIP64/spanning archive options when building releases","Reject non-200 responses instead of parsing their bodies as archives"],"tags":["zip","validation","security","corrupt-download"],"backgroundTag":"checksum-mismatch","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}