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.

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.

Appendix: 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)