{"record":{"id":"f03eb44d3119ac79","repo":"Hmbown/CodeWhale","slug":"the-update-identity-is-invalid","errorCode":null,"errorMessage":"The update identity is invalid.","messagePattern":"The update identity is invalid\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/app/updates.mjs","lineNumber":86,"sourceCode":"    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\"});\n    if(result.status!==0) throw new Error(\"The update could not be unpacked.\");","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/app/updates.mjs#L68-L104","documentation":"prepareUpdate() in the computer-use plugin validates the update descriptor passed to it before downloading anything. The descriptor must have available=true, a version strictly newer than the running APP_VERSION, a url exactly matching the fixed GitHub release asset URL for that version, and a size that is a safe positive integer within the download limit. Any deviation means the descriptor was not produced by this app's own release check, so it throws rather than downloading untrusted bytes.","triggerScenarios":"Calling prepareUpdate() with an update object where update.available is truthy but: update.version is not newer than APP_VERSION (or equal/older), update.url differs from `${repository}/releases/download/v${version}/Codewhale-Computer-Use-${version}-macos-universal.zip`, or update.size is missing, negative, zero, non-integer, non-safe-integer, or above the size limit.","commonSituations":"Hand-constructing or persisting a stale update descriptor (e.g. cached metadata from an older release whose asset URL pattern changed); a MITM'd or tampered metadata source; calling prepareUpdate after the app already updated to the version in the descriptor; a typo'd or proxy-rewritten download URL in custom update metadata.","solutions":["Re-run the check-for-update step so prepareUpdate() receives a fresh, app-generated descriptor instead of cached or hand-built metadata.","Confirm update.url matches the exact expected GitHub release asset URL pattern for update.version, including the repository base and the -macos-universal.zip suffix.","Verify update.version is strictly newer than the running APP_VERSION; if the app is already current, skip the update flow.","Ensure update.size is present and a positive safe integer within the configured download limit (re-derive it from the release asset metadata)."],"exampleFix":"// before\nawait prepareUpdate({available:true, version:\"1.2.3\", url:\"https://mirror.example.com/app.zip\", size:null});\n// after\nawait prepareUpdate({available:true, version:\"1.4.0\", url:`${repository}/releases/download/v1.4.0/Codewhale-Computer-Use-1.4.0-macos-universal.zip`, size:48213376, sha256:\"<hex>\"});","handlingStrategy":"validation","validationCode":"const expectedUrl = `${repository}/releases/download/v${u.version}/Codewhale-Computer-Use-${u.version}-macos-universal.zip`;\nif (!u?.available || !newerVersion(u.version, APP_VERSION) || u.url !== expectedUrl || !Number.isSafeInteger(u.size) || u.size <= 0 || u.size > limit) skipUpdate();","typeGuard":"function isValidUpdate(u) { return !!u && u.available === true && typeof u.version === 'string' && typeof u.url === 'string' && Number.isSafeInteger(u.size) && u.size > 0 && typeof u.sha256 === 'string'; }","tryCatchPattern":"try { await prepareUpdate(update); } catch (e) { if (e.message === 'The update identity is invalid.') { /* re-run check-for-update or skip */ } else throw e; }","preventionTips":["Always feed prepareUpdate the descriptor produced by the app's own update check, never cached or hand-built metadata.","Validate the descriptor shape (version/url/size/sha256) as soon as it is received.","Drop the descriptor once the app version reaches or exceeds it."],"tags":["update","validation","security","metadata"],"backgroundTag":"invalid-argument-value","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"}