{"record":{"id":"bbd407318eb1f170","repo":"paperclipai/paperclip","slug":"teams-upload-is-not-confirmed","errorCode":null,"errorMessage":"Teams upload is not confirmed","messagePattern":"Teams upload is not confirmed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/chat-teams-file-consent.ts","lineNumber":414,"sourceCode":"      .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(\n    operation: \"put\" | \"status\",\n    bytes: Buffer | null,\n    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","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-teams-file-consent.ts#L396-L432","documentation":"fileInfo() builds the Teams 'file info' attachment card (contentType application/vnd.microsoft.teams.card.file.info) that is sent after the OneDrive upload completes. It refuses to run unless the capability's #confirmed flag is true, i.e. a previous exchange('put') classified the upload as uploaded. This prevents sending a download link for a file that was never actually committed to the recipient's OneDrive.","triggerScenarios":"Calling fileInfo() (directly or via buildTeamsUploadedFileCard) on an UploadCapability that was restored with confirmed=false, or on a freshly created capability whose exchange('put') has not yet returned { kind: 'uploaded' }, or whose PUT ended uncertain/incomplete.","commonSituations":"Worker resumes after restart and builds the file card before re-running the upload; caller ignores the exchange() outcome (uncertain/incomplete) and proceeds to send the card; the upload session expired and returned session_unavailable but downstream code treats the flow as done.","solutions":["Only call fileInfo()/buildTeamsUploadedFileCard after exchange('put') returned an outcome with kind === 'uploaded'.","After restoring from sealed state, check the confirmed flag first (via outcome of a status operation or by gating on the persisted phase reaching file_info_pending) before building the card.","If the upload is incomplete, re-drive the state machine: exchange('put') or exchange('status') until kind === 'uploaded', advancing phases via nextTeamsFileConsentPhase.","If the session expired (session_unavailable), restart the whole consent flow with a fresh binding rather than fabricating a file card."],"exampleFix":"// before\nconst card = buildTeamsUploadedFileCard(upload, outcome); // throws if not uploaded\n\n// after\nif (outcome.kind !== \"uploaded\") {\n  throw new Error(`upload not complete: ${outcome.kind}${\"reason\" in outcome ? \"/\" + outcome.reason : \"\"}`);\n}\nconst card = buildTeamsUploadedFileCard(upload, outcome);","handlingStrategy":"type-guard","validationCode":"const outcome = await exchangeTeamsFileUpload({ upload, binding, operation: \"status\" });\nif (outcome.kind !== \"uploaded\") throw new Error(\"upload not confirmed; cannot send file card\");","typeGuard":"function isConfirmedOutcome(outcome: TeamsUploadOutcome): outcome is Extract<TeamsUploadOutcome, { kind: \"uploaded\" }> {\n  return outcome.kind === \"uploaded\";\n}","tryCatchPattern":"try {\n  const card = buildTeamsUploadedFileCard(upload, outcome);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Teams upload is not confirmed\") {\n    // re-drive exchange(\"put\")/exchange(\"status\") until uploaded, or restart flow\n    return null;\n  }\n  throw e;\n}","preventionTips":["Gate file-card sending on the persisted phase being file_info_pending/delivered via nextTeamsFileConsentPhase.","Never call fileInfo() directly on a restored capability without checking confirmation state first.","Handle uncertain/incomplete exchange outcomes explicitly; do not fall through to card building.","On session_unavailable, restart the consent flow instead of sending a dead link."],"tags":["teams","invalid-state-transition","upload","state-machine"],"backgroundTag":"invalid-state-transition","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"}