{"record":{"id":"1b92cb1600af7337","repo":"Hmbown/CodeWhale","slug":"check-for-an-available-update-first","errorCode":null,"errorMessage":"Check for an available update first.","messagePattern":"Check for an available update first\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/app/updates.mjs","lineNumber":85,"sourceCode":"    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\n    // anything, including a compressed payload whose headers understate size.\n    const payload=bytes.subarray(start,start+compressed);\n    let expanded;\n    try { expanded=method===0?payload.length:inflateRawSync(payload,{maxOutputLength:Math.max(size,1)}).length; }\n    catch { throw new Error(\"Invalid or oversized compressed update entry.\"); }\n    if(expanded!==size) throw new Error(\"The update entry size did not match its contents.\");\n    position+=46+length+extra+comment;\n  }\n  if(position!==end) throw new Error(\"Invalid update archive length.\");\n  return count;\n}\n\nexport async function prepareUpdate(update) {\n  if(!update?.available) throw new Error(\"Check for an available update first.\");\n  if(!newerVersion(update.version,APP_VERSION)||update.url!==`${repository}/releases/download/v${update.version}/Codewhale-Computer-Use-${update.version}-macos-universal.zip`||!Number.isSafeInteger(update.size)||update.size<=0||update.size>limit) throw new Error(\"The update identity is invalid.\");\n  // Only GitHub's fixed release URL and its asset CDN can serve the bytes.\n  let url=update.url, response;\n  for(let redirects=0;redirects<4;redirects++) {\n    response=await fetch(url,{redirect:\"manual\",signal:AbortSignal.timeout(60_000)});\n    if(![301,302,303,307,308].includes(response.status)) break;\n    const next=new URL(response.headers.get(\"location\"),url);\n    if(next.protocol!==\"https:\"||![\"github.com\",\"release-assets.githubusercontent.com\",\"objects.githubusercontent.com\"].includes(next.hostname)) throw new Error(\"The update download redirected to an unexpected host.\");\n    url=next.href;\n  }\n  if(!response?.ok) throw new Error(\"The update could not be downloaded. Your current app is unchanged.\");\n  const bytes=await responseBytes(response,update.size);\n  if(bytes.length!==update.size||crypto.createHash(\"sha256\").update(bytes).digest(\"hex\")!==update.sha256) throw new Error(\"The update checksum did not match. Your current app is unchanged.\");\n  validateReleaseZip(bytes);\n  const stage=fs.mkdtempSync(path.join(os.tmpdir(),\"codewhale-cu-release-\"));\n  try {\n    const archive=path.join(stage,\"release.zip\"); fs.writeFileSync(archive,bytes,{mode:0o600});\n    const result=spawnSync(\"ditto\",[\"-x\",\"-k\",archive,stage],{encoding:\"utf8\"});","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/app/updates.mjs#L67-L103","documentation":"prepareUpdate requires an update object previously produced by checkForUpdate/releaseUpdate (which sets `available:true` plus version, url, sha256 and size). Passing undefined/null or an object from a check that found nothing newer throws immediately, before any download or identity validation. The library never assumes a default update; the caller must run the check first.","triggerScenarios":"Calling prepareUpdate() with no argument, with `null`/`undefined`, or with the object returned when no update was available (`{available:false,...}`); storing the check result and calling prepareUpdate after it resolved to available:false.","commonSituations":"UI code invoking install on startup before checkForUpdate resolves; a user clicking 'Install' when the earlier check said 'no newer version'; passing a hand-rolled object missing the `available` flag; race where the stored result came from an older check.","solutions":["Call checkForUpdate() and only invoke prepareUpdate with its result when `result.available === true`.","Persist the full result object (including `available`, `version`, `url`, `sha256`, `size`) and pass it verbatim; don't reconstruct a partial object.","Gate the install UI on the availability message so the action can't fire when no update was found.","If the result may be stale, re-run checkForUpdate immediately before prepareUpdate."],"exampleFix":"// before\nconst update = readUpdateResult();\nawait prepareUpdate(update); // may be {available:false}\n// after\nconst update = readUpdateResult();\nif (update?.available) await prepareUpdate(update);","handlingStrategy":"type-guard","validationCode":"if (!update || update.available !== true) throw new Error(\"Run checkForUpdate first; only available updates can be prepared\");","typeGuard":"function isAvailableUpdate(u){return !!u && typeof u === \"object\" && u.available === true && typeof u.url === \"string\" && typeof u.sha256 === \"string\" && typeof u.version === \"string\";}","tryCatchPattern":"try { await prepareUpdate(update); } catch (e) { if (e.message === \"Check for an available update first.\") { await checkForUpdate().then(u => u.available ? prepareUpdate(u) : notify(u.message)); } else throw e; }","preventionTips":["Persist the whole update object from checkForUpdate, not a subset","Gate the install action on `update.available === true`","Re-check for updates if the stored result may be stale","Never construct update objects by hand"],"tags":["update-flow","state-error","api-misuse","precondition"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T21:17:16.096Z"}