{"record":{"id":"c8bf31b11226e358","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-source-integrity-mismatch","errorCode":"paperclip_runner_chat_attachment_source_integrity_mismatch","errorMessage":"paperclip_runner_chat_attachment_source_integrity_mismatch","messagePattern":"paperclip_runner_chat_attachment_source_integrity_mismatch","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":1320,"sourceCode":"        throw new Error(\n          \"paperclip_runner_chat_attachment_source_size_mismatch\",\n        );\n      }\n      chunks.push(buffer);\n    }\n  } finally {\n    clearTimeout(timeout);\n  }\n  const body = Buffer.concat(chunks);\n  if (\n    body.length !== source.byteSize ||\n    createHash(\"sha256\").update(body).digest(\"hex\") !==\n      source.sha256.toLowerCase()\n  ) {\n    if (!object.stream.destroyed) {\n      object.stream.destroy();\n    }\n    throw new Error(\n      \"paperclip_runner_chat_attachment_source_integrity_mismatch\",\n    );\n  }\n  return body;\n}\n\nconst DEFAULT_STORAGE_TIMEOUT_MS = 10_000;\n\nasync function deleteStorageObjectWithin(\n  storage: StorageService,\n  companyId: string,\n  objectKey: string,\n  timeoutMs: number,\n): Promise<void> {\n  const deletion = storage\n    .deleteObject(companyId, objectKey)\n    .catch(() => undefined);\n  let timer: ReturnType<typeof setTimeout> | null = null;","sourceCodeStart":1302,"sourceCodeEnd":1338,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L1302-L1338","documentation":"After fully reading the source object in readSourceBytes, the buffer length and its SHA-256 digest are compared to source.byteSize and source.sha256. If either does not match, the stream is destroyed and this integrity error is thrown. It guards against reusing a storage object whose content no longer matches the snapshot metadata the reuse decision was based on.","triggerScenarios":"storage.getObject returns content whose SHA-256 differs from source.sha256 (object mutated/replaced under the same key), whose length differs from source.byteSize (truncated write), or whose hash was stored with different casing than the computed lowercase hex (source.sha256 not lowercase), producing digest comparison failure.","commonSituations":"S3 eventual consistency or a failed prior overwrite left stale content at the key; a caller supplied a sha256 with uppercase characters since the code lowercases only the right-hand side; object key reuse across versions after a re-upload; database backup/restore mismatch between attachment metadata and object store contents.","solutions":["Recompute the object's sha256 with `sha256sum` / crypto and update or regenerate the source record so its sha256/byteSize match storage","Re-upload the attachment to a fresh objectKey and reference the new key in the source","Normalize source.sha256 to lowercase hex before building the ChatAttachmentReuseSource (the comparison lowercases the stored value but the digest is lowercase hex)","Audit for code paths that overwrite objects at existing keys and enforce write-once keys"],"exampleFix":"// before\nsource.sha256 = hash.toUpperCase();\n// after\nsource.sha256 = createHash(\"sha256\").update(body).digest(\"hex\").toLowerCase();","handlingStrategy":"validation","validationCode":"const hash = createHash(\"sha256\").update(body).digest(\"hex\").toLowerCase();\nif (hash !== source.sha256.toLowerCase() || body.length !== source.byteSize) throw new Error(\"source object content diverges from recorded sha256/byteSize\");","typeGuard":"function hasValidDigest(s) {\n  return typeof s.sha256 === \"string\" && /^[0-9a-f]{64}$/.test(s.sha256.toLowerCase()) && s.sha256 === s.sha256.toLowerCase();\n}","tryCatchPattern":"try {\n  await prepareReusedChatAttachment({ db, binding, source, title });\n} catch (err) {\n  if (err.message === \"paperclip_runner_chat_attachment_source_integrity_mismatch\") {\n    // re-upload the source to a fresh key and rebuild the source record before retrying\n  } else throw err;\n}","preventionTips":["Store sha256 as lowercase hex everywhere; normalize on ingest","Re-verify object hash after any backup/restore or key migration","Never overwrite objects in place; upload to a new key on content change","Compute hashes from the exact bytes persisted, not pre-transformation input"],"tags":["storage","integrity","checksum","sha256"],"backgroundTag":"checksum-mismatch","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}