paperclipai/paperclip · error
Invalid Teams upload state
Error message
Invalid Teams upload state
What it means
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.
Source
Thrown at server/src/services/chat-teams-file-consent.ts:404
)
throw new Error("Invalid Teams upload binding");
const value = z
.object({
bindingDigest: z.string(),
info: uploadInfoSchema,
confirmed: z.boolean(),
putStarted: z.boolean(),
})
.strict()
.parse(await openPrivate(context, "upload", material));
if (
value.bindingDigest !== digest(binding) ||
value.info.name !== binding.filename ||
typeof sharePointUrl(value.info.uploadUrl) === "string" ||
typeof sharePointUrl(value.info.contentUrl, true) === "string" ||
(value.confirmed && !value.putStarted)
)
throw new Error("Invalid Teams upload state");
const result = new UploadCapability(value.info, binding);
result.#confirmed = value.confirmed;
result.#putStarted = value.putStarted;
return result;
}
matches(binding: TeamsFileConsentBinding): boolean {
return this.#bindingDigest === digest(binding);
}
fileInfo() {
if (!this.#confirmed) throw new Error("Teams upload is not confirmed");
return {
contentType: "application/vnd.microsoft.teams.card.file.info" as const,
name: this.#info.name,
contentUrl: this.#info.contentUrl,
content: { uniqueId: this.#info.uniqueId, fileType: this.#info.fileType },
};
}
async exchange(View on GitHub (pinned to 01ad858492)
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.
Example fix
// before
const upload = await UploadCapability.restore(ctx, binding, material); // throws on stale binding
// after
const current = parseTeamsFileConsentBinding(binding);
const stored = parseTeamsFileConsentBinding(storedBinding);
if (!current || !stored || digest(current) !== digest(stored)) {
// binding was regenerated since sealing; abandon the old upload state
return null;
}
const upload = await UploadCapability.restore(ctx, current, material); Defensive patterns
Strategy: validation
Validate before calling
import { createHash } from "node:crypto";
const digest = (v) => createHash("sha256").update(JSON.stringify(v)).digest("hex");
if (digest(storedBinding) !== digest(currentBinding)) {
return null; // sealed upload state is stale; do not attempt restore
} Type guard
function bindingMatches(stored: unknown, current: TeamsFileConsentBinding): stored is TeamsFileConsentBinding {
const p = parseTeamsFileConsentBinding(stored);
return !!p && createHash("sha256").update(JSON.stringify(p)).digest("hex") === digest(current);
} Try / catch
try {
return await UploadCapability.restore(context, binding, material);
} catch (e) {
if (e instanceof Error && e.message === "Invalid Teams upload state") {
return null; // discard corrupt/stale sealed state, restart upload flow
}
throw e;
} Prevention
- 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).
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Invalid Teams file consent binding
- Teams private state could not be restored
- Configuration does not match the plugin's instanceConfigSche
- createServerAdapter() from "${packageName}" returned an inva
- Invalid worktree seed manifest at ${manifestPath}.
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/92aff9c98c76527c.
Report an issue: GitHub.