{"record":{"id":"92aff9c98c76527c","repo":"paperclipai/paperclip","slug":"invalid-teams-upload-state","errorCode":null,"errorMessage":"Invalid Teams upload state","messagePattern":"Invalid Teams upload state","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/chat-teams-file-consent.ts","lineNumber":404,"sourceCode":"    )\n      throw new Error(\"Invalid Teams upload binding\");\n    const value = z\n      .object({\n        bindingDigest: z.string(),\n        info: uploadInfoSchema,\n        confirmed: z.boolean(),\n        putStarted: z.boolean(),\n      })\n      .strict()\n      .parse(await openPrivate(context, \"upload\", material));\n    if (\n      value.bindingDigest !== digest(binding) ||\n      value.info.name !== binding.filename ||\n      typeof sharePointUrl(value.info.uploadUrl) === \"string\" ||\n      typeof sharePointUrl(value.info.contentUrl, true) === \"string\" ||\n      (value.confirmed && !value.putStarted)\n    )\n      throw new Error(\"Invalid Teams upload state\");\n    const result = new UploadCapability(value.info, binding);\n    result.#confirmed = value.confirmed;\n    result.#putStarted = value.putStarted;\n    return result;\n  }\n  matches(binding: TeamsFileConsentBinding): boolean {\n    return this.#bindingDigest === digest(binding);\n  }\n  fileInfo() {\n    if (!this.#confirmed) throw new Error(\"Teams upload is not confirmed\");\n    return {\n      contentType: \"application/vnd.microsoft.teams.card.file.info\" as const,\n      name: this.#info.name,\n      contentUrl: this.#info.contentUrl,\n      content: { uniqueId: this.#info.uniqueId, fileType: this.#info.fileType },\n    };\n  }\n  async exchange(","sourceCodeStart":386,"sourceCodeEnd":422,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-teams-file-consent.ts#L386-L422","documentation":"After decrypting the sealed upload state, UploadCapability.restore() validates it: the recorded bindingDigest must equal the digest of the current binding, the stored filename must match, uploadUrl/contentUrl must still be valid HTTPS SharePoint URLs (sharePointUrl returning a string means a rejection, which is a failure here), and a confirmed state must have putStarted. This error means the decrypted state is inconsistent with the binding or was tampered with/corrupted.","triggerScenarios":"Restoring where: value.bindingDigest !== digest(binding) (binding was re-created or mutated, e.g. expiresAt/sourceGeneration changed after sealing); value.info.name !== binding.filename; uploadUrl or contentUrl now fails SharePoint URL validation (non-https, wrong host, port, query on contentUrl); or confirmed=true with putStarted=false (impossible state).","commonSituations":"Schema/version changes that alter the binding shape so its digest no longer matches; DB rows where the binding was regenerated on retry while the old sealed upload was restored; hand-edited or migrated provider-state rows; SharePoint URLs from a government/non-commercial tenant host that the conservative sharepoint.com-only allowlist rejects.","solutions":["Confirm you are passing the exact same TeamsFileConsentBinding object (same field values, unmodified) that was used when the capability was sealed; do not rebuild or normalize it.","Verify the sealed material was produced by upload.seal() for the same binding — a digest mismatch means the persisted state is stale and the flow should restart.","Check the stored uploadUrl/contentUrl: they must be https *.sharepoint.com URLs without ports, credentials, fragments, or (for contentUrl) query strings; reject government/regional tenants that use other hosts.","If confirmed=true/putStarted=false appears, the sealed record is corrupt — discard it and re-run the upload from upload_pending."],"exampleFix":"// before\nconst upload = await UploadCapability.restore(ctx, binding, material); // throws on stale binding\n\n// after\nconst current = parseTeamsFileConsentBinding(binding);\nconst stored = parseTeamsFileConsentBinding(storedBinding);\nif (!current || !stored || digest(current) !== digest(stored)) {\n  // binding was regenerated since sealing; abandon the old upload state\n  return null;\n}\nconst upload = await UploadCapability.restore(ctx, current, material);","handlingStrategy":"validation","validationCode":"import { createHash } from \"node:crypto\";\nconst digest = (v) => createHash(\"sha256\").update(JSON.stringify(v)).digest(\"hex\");\nif (digest(storedBinding) !== digest(currentBinding)) {\n  return null; // sealed upload state is stale; do not attempt restore\n}","typeGuard":"function bindingMatches(stored: unknown, current: TeamsFileConsentBinding): stored is TeamsFileConsentBinding {\n  const p = parseTeamsFileConsentBinding(stored);\n  return !!p && createHash(\"sha256\").update(JSON.stringify(p)).digest(\"hex\") === digest(current);\n}","tryCatchPattern":"try {\n  return await UploadCapability.restore(context, binding, material);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Invalid Teams upload state\") {\n    return null; // discard corrupt/stale sealed state, restart upload flow\n  }\n  throw e;\n}","preventionTips":["Persist the binding immutably alongside the sealed upload state; never update one without resealing the other.","Version the sealed envelope schema so migrations invalidate old state explicitly.","Treat restore failure as 'restart the transfer', not as retryable.","Test restore round-trips after any change to binding fields (digest is field-order/value sensitive)."],"tags":["teams","schema-validation","state-restore","corrupt-state"],"backgroundTag":"schema-validation-failed","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"}