Hmbown/CodeWhale · error · Error

Invalid update archive.

Error message

Invalid update archive.

What it means

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).

Solutions

  1. Re-download the release asset and confirm its SHA-256 matches the digest published on the release
  2. Inspect the first bytes (PK\u0003\u0004 signature) to confirm the file is actually a ZIP, not an HTML error page
  3. Repackage without ZIP64/spanning options if the archive was built with them
  4. Verify asset size against the release metadata before validation

Example fix

// before (validate whatever came back)
code.validateReleaseZip(bytes);
// after (verify digest first)
if (code.createHash("sha256").update(bytes).digest("hex") !== expectedSha) throw new Error("download digest mismatch");
code.validateReleaseZip(bytes);
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeZip(bytes) {
  return bytes.length >= 22 && bytes.readUInt32LE(0) === 0x04034b50;
}

Try / catch

try {
  validateReleaseZip(bytes);
} catch (e) {
  if (e.message === "Invalid update archive.") {
    discardDownload(); // corrupt or hostile; do not extract
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/f7364ea829ebdc5b. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/updates.mjs:53

  if(!version||release.draft||release.prerelease||!newerVersion(version,current)) return {available:false,message:`You have Computer Use ${current}. No newer stable installer is available.`};
  const name=`Codewhale-Computer-Use-${version}-macos-universal.zip`;
  const asset=release.assets?.find(asset=>asset.name===name);
  const url=`${repository}/releases/download/v${version}/${name}`;
  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.`};
  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.`};
}
export async function checkForUpdate() {
  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)});
  if(response.status===404) return {available:false,message:"No stable installer has been published yet. Your current app is unchanged."};
  if(!response.ok) throw new Error(`The update service is unavailable (${response.status}). Try again later.`);
  return releaseUpdate(JSON.parse((await responseBytes(response,1024*1024)).toString("utf8")));
}

/** Inspect both ZIP headers before extraction: no links, traversal or bombs. */
export function validateReleaseZip(bytes) {
  const minimum=Math.max(0,bytes.length-65557); let end=-1;
  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; }
  if(end<0||bytes.readUInt16LE(end+4)||bytes.readUInt16LE(end+6)) throw new Error("Invalid update archive.");
  const count=bytes.readUInt16LE(end+10); let position=bytes.readUInt32LE(end+16),total=0;
  if(!count||count>2000||bytes.readUInt16LE(end+8)!==count||position+bytes.readUInt32LE(end+12)!==end) throw new Error("Invalid update archive index.");
  const seen=new Set();
  for(let i=0;i<count;i++) {
    if(position+46>end||bytes.readUInt32LE(position)!==0x02014b50) throw new Error("Invalid update entry.");
    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);
    const name=bytes.subarray(position+46,position+46+length).toString("utf8");
    const kind=(bytes.readUInt32LE(position+38)>>>16)&0xf000,offset=bytes.readUInt32LE(position+42),compressed=bytes.readUInt32LE(position+20);
    const size=bytes.readUInt32LE(position+24); total+=size;
    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.");
    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.");
    seen.add(name);
    if(offset+30>position||bytes.readUInt32LE(offset)!==0x04034b50) throw new Error("Invalid update file header.");
    const localLength=bytes.readUInt16LE(offset+26),localExtra=bytes.readUInt16LE(offset+28);
    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.");
    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.");
    const start=offset+30+localLength+localExtra;
    // Header sizes are untrusted. Bound actual expansion before ditto writes

View on GitHub (pinned to 73e0f67d83)