paperclipai/paperclip · error

Teams private state could not be restored

Error message

Teams private state could not be restored

What it means

openPrivate decrypts and re-validates sealed Teams file-consent private state (binding, upload, response envelopes) using the local_encrypted secret provider. It throws this generic message for ANY failure inside the restore path: malformed ciphertext, oversized material, wrong schema/purpose, context digest mismatch, or provider decryption failure. The original cause is deliberately swallowed so no plaintext or provider detail leaks.

Source

Thrown at server/src/services/chat-teams-file-consent.ts:152

    );
    if (Buffer.byteLength(plaintext) > 64 * 1024) throw new Error();
    const envelope = z
      .object({
        schema: z.literal("paperclip.teams.file-private.v1"),
        context: privateContextSchema,
        purpose: z.string(),
        value: z.unknown(),
      })
      .strict()
      .parse(JSON.parse(plaintext));
    if (
      envelope.purpose !== purpose ||
      digest(envelope.context) !== digest(parsedContext)
    )
      throw new Error();
    return envelope.value;
  } catch {
    throw new Error("Teams private state could not be restored");
  }
}

export async function sealTeamsFileConsentBinding(
  context: TeamsFilePrivateContext,
  binding: TeamsFileConsentBinding,
) {
  const parsed = parseTeamsFileConsentBinding(binding);
  if (
    !parsed ||
    parsed.companyId !== context.companyId ||
    parsed.endpointId !== context.endpointId
  )
    throw new Error("Invalid Teams private binding");
  return sealPrivate(context, "binding", parsed);
}

export async function restoreTeamsFileConsentBinding(

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the TeamsFilePrivateContext passed in exactly matches the one used at seal time: companyId, endpointId, transferId and authorityDigest must all be identical (digest comparison is strict).
  2. Confirm you are passing the correct TeamsFileCiphertext loaded from the exact locked transfer row, and the purpose matches the API (binding material to restoreTeamsFileConsentBinding, upload material to restoreTeamsFileUpload).
  3. Check the local_encrypted secret provider is configured with the same key that sealed the state; re-seal if keys were rotated.
  4. If state cannot be restored legitimately, treat it as unrecoverable per the module's contract: restart the flow with a fresh consent card and a new sealed binding rather than retrying restore.

Example fix

// before
const binding = await restoreTeamsFileConsentBinding(currentTransferContext, materialFromOtherRow);
// after
const binding = await restoreTeamsFileConsentBinding(originalSealContext, materialFromSameRow);
Defensive patterns

Strategy: try-catch

Validate before calling

import { z } from 'zod';
const ctx = z.object({ companyId: z.string().uuid(), endpointId: z.string().uuid(), transferId: z.string().uuid(), authorityDigest: z.string().regex(/^[a-f0-9]{64}$/) }).safeParse(context);
const sized = material && JSON.stringify(material).length <= 128 * 1024;
if (!ctx.success || !sized) throw new Error('context or material invalid before restore');

Type guard

function isSealableContext(c: unknown): c is TeamsFilePrivateContext {
  return typeof c === 'object' && c !== null &&
    'companyId' in c && 'endpointId' in c && 'transferId' in c &&
    'authorityDigest' in c;
}

Try / catch

try {
  const binding = await restoreTeamsFileConsentBinding(context, material);
} catch (err) {
  if (err instanceof Error && err.message === 'Teams private state could not be restored') {
    // start a fresh consent flow; do not retry restore blindly
  }
}

Prevention

When it happens

Trigger: Calling restoreTeamsFileConsentBinding / UploadCapability.restore / ConsentEvent.restore with (a) ciphertext that fails the secret provider's resolveVersion, (b) material JSON >128KB or plaintext >64KB, (c) an envelope whose purpose does not match ('binding'/'upload'/'response'), or (d) an envelope.context digest differing from the current TeamsFilePrivateContext (companyId/endpointId/transferId/authorityDigest changed).

Common situations: Restoring state after a server restart against a database row whose context changed (endpoint re-provisioned, new transferId, rotated authorityDigest); pointing a restore call at the wrong row's ciphertext; mixing 'binding' material into an 'upload' restore; tampered or hand-edited DB rows; secret-provider key rotation making old ciphertext undecryptable.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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