{"record":{"id":"b4a868c542b78161","repo":"paperclipai/paperclip","slug":"teams-upload-bytes-do-not-match-consent","errorCode":null,"errorMessage":"Teams upload bytes do not match consent","messagePattern":"Teams upload bytes do not match consent","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/chat-teams-file-consent.ts","lineNumber":443,"sourceCode":"    options: UploadRequestOptions,\n  ): Promise<TeamsUploadOutcome> {\n    if (\n      (operation !== \"put\" && operation !== \"status\") ||\n      options.byteSize !== this.#byteSize\n    )\n      throw new Error(\"Invalid Teams upload binding\");\n    // The capability is itself a security boundary; calling it directly cannot\n    // bypass the outer convenience function's exact-byte checks or mutate a\n    // caller-owned Buffer while current authorization is awaited.\n    const snapshot =\n      operation === \"put\" && Buffer.isBuffer(bytes) ? Buffer.from(bytes) : null;\n    if (\n      operation === \"put\" &&\n      (!snapshot ||\n        snapshot.length !== this.#byteSize ||\n        createHash(\"sha256\").update(snapshot).digest(\"hex\") !== this.#sha256)\n    )\n      throw new Error(\"Teams upload bytes do not match consent\");\n    const controller = new AbortController();\n    const signal = options.signal\n      ? AbortSignal.any([options.signal, controller.signal])\n      : controller.signal;\n    const timer = setTimeout(() => controller.abort(), 30_000);\n    try {\n      await abortable(Promise.resolve().then(options.authorize), signal);\n      signal.throwIfAborted();\n      if (this.#expiresAt <= Date.now())\n        return { kind: \"uncertain\", reason: \"session_unavailable\" };\n      if (operation === \"put\") {\n        if (this.#confirmed) return { kind: \"uploaded\" };\n        if (this.#putStarted)\n          return { kind: \"uncertain\", reason: \"put_already_attempted\" };\n        this.#putStarted = true;\n      }\n      const response = await abortable(\n        (options.request ?? guardedRemoteHttpFetch)(","sourceCodeStart":425,"sourceCodeEnd":461,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-teams-file-consent.ts#L425-L461","documentation":"For operation 'put', exchange() requires an actual Buffer snapshot whose length equals the consented byteSize and whose SHA-256 equals the sha256 recorded in the consent binding. This error means the bytes offered for upload are not exactly the bytes the user consented to receive — either no/invalid buffer, a size mismatch, or different content. It is an integrity guard so the uploaded file can never diverge from the consent card shown to the user.","triggerScenarios":"exchange('put', bytes, ...) where bytes is not a Buffer (snapshot null), snapshot.length !== this.#byteSize, or sha256(snapshot) !== this.#sha256. Happens when the source file was modified or re-encoded after the binding was created, the wrong file/buffer is passed, or the caller hashed a different variant (e.g. normalized line endings) than the binding's digest.","commonSituations":"Attachment content regenerated between consent grant and upload (digest recorded at publication time, file changed since); passing a string or Blob instead of Buffer; streaming pipeline that dropped or added bytes; retrieving the wrong attachment row so size or hash differs.","solutions":["Recompute sha256 and byte length from the exact buffer you pass and compare against binding.sha256/binding.byteSize before calling exchange; if they differ, refresh the source attachment rather than forcing the upload.","Pass a Node Buffer (not string/Uint8Array view of different content) — exchange snapshots it via Buffer.from.","If the file legitimately changed, create a new consent binding and send a new consent card; the old consent is bound to the old bytes.","Check that the digest was computed with createHash('sha256').update(buffer).digest('hex') on the same bytes actually uploaded (no encoding conversion)."],"exampleFix":"// before\nawait exchangeTeamsFileUpload({ upload, binding, operation: \"put\", bytes: maybeStaleBuffer });\n\n// after\nconst bytes = await loadAttachmentBytes(attachmentId); // fresh read\nconst sha256 = createHash(\"sha256\").update(bytes).digest(\"hex\");\nif (bytes.length !== binding.byteSize || sha256 !== binding.sha256) {\n  throw new Error(\"source attachment changed since consent; re-issue consent card\");\n}\nawait exchangeTeamsFileUpload({ upload, binding, operation: \"put\", bytes });","handlingStrategy":"validation","validationCode":"import { createHash } from \"node:crypto\";\nconst sha256 = createHash(\"sha256\").update(bytes).digest(\"hex\");\nif (!(bytes instanceof Buffer) || bytes.length !== binding.byteSize || sha256 !== binding.sha256) {\n  throw new Error(\"upload bytes no longer match consented content\");\n}","typeGuard":"function matchesConsent(bytes: unknown, binding: TeamsFileConsentBinding): bytes is Buffer {\n  return bytes instanceof Buffer &&\n    bytes.length === binding.byteSize &&\n    createHash(\"sha256\").update(bytes).digest(\"hex\") === binding.sha256;\n}","tryCatchPattern":"try {\n  await exchangeTeamsFileUpload({ upload, binding, operation: \"put\", bytes });\n} catch (e) {\n  if (e instanceof Error && e.message === \"Teams upload bytes do not match consent\") {\n    // source changed since consent: re-issue the consent card with a new binding\n    return { kind: \"uncertain\", reason: \"consent_stale\" };\n  }\n  throw e;\n}","preventionTips":["Compute and store sha256 at publication time and re-verify immediately before upload.","Freeze/snapshot the attachment bytes at consent creation so later edits cannot slip in.","Compare digest + size from the binding only; never trust caller-supplied metadata.","If a mismatch is detected, always re-run consent with a fresh binding instead of forcing the PUT."],"tags":["teams","integrity","checksum","upload"],"backgroundTag":"checksum-mismatch","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}