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

  1. 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.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/92aff9c98c76527c. Report an issue: GitHub.