paperclipai/paperclip · error · PaperclipCloudConnectorError

REAUTHORIZATION_REQUIRED

REAUTHORIZATION_REQUIRED

Error message

Paperclip Cloud scope grant did not match

What it means

The stored Paperclip Cloud credential's granted scopes no longer match the scopes required by the connector profile definition (sameStringSet(credentials.scopes, definition.scopes) fails). This is thrown by openCredentials, which validates every stored binding before use by claim/refresh. The code is REAUTHORIZATION_REQUIRED, meaning the user must re-run the authorization flow to obtain a fresh scope grant.

Source

Thrown at server/src/services/paperclip-cloud-connector.ts:309

      config.environment,
      definition.provider,
      profile,
      definition.scopes,
    );
    if (
      credentials.instanceId !== config.instanceId
      || credentials.environment !== config.environment
      || credentials.subject !== subject
      || credentials.companyId !== companyId
      || credentials.provider !== definition.provider
    ) {
      throw new PaperclipCloudConnectorError("Paperclip Cloud credential binding did not match", "CONNECTOR_BINDING_MISMATCH");
    }
    if (credentials.profile !== profile) {
      throw new PaperclipCloudConnectorError("Paperclip Cloud connector profile binding did not match", "CONNECTOR_BINDING_MISMATCH");
    }
    if (!sameStringSet(credentials.scopes, definition.scopes)) {
      throw new PaperclipCloudConnectorError("Paperclip Cloud scope grant did not match", "REAUTHORIZATION_REQUIRED");
    }
    return credentials;
  }

  return {
    async getInstanceStatus(): Promise<"active" | "suspended" | "removed"> {
      let response: ConnectorResponse;
      try {
        response = await call("status", {
          subject: "instance-status",
          companyId: "instance-status",
        });
      } catch (error) {
        if (error instanceof PaperclipCloudConnectorError && error.status === 401) return "removed";
        throw error;
      }
      if (response.status === "active" && response.active === true) return "active";
      if (response.status === "suspended" && response.active === false) return "suspended";

View on GitHub (pinned to 01ad858492)

Solutions

  1. Re-run the authorization flow (startAuthorization) so the user grants the current definition scopes
  2. Diff credentials.scopes against definition.scopes to identify which scope is missing/extra, then update the connector definition or re-consent
  3. If the definition change was unintentional, revert the scope list in the connector definition so it matches existing grants
  4. Verify the broker is returning the full requested scope set and not a reduced one

Example fix

// before (stale grant after definition change)
const creds = await connector.claim({ subject, companyId }); // throws REAUTHORIZATION_REQUIRED

// after (detect and re-authorize)
try {
  const creds = await connector.claim({ subject, companyId });
} catch (e) {
  if (e instanceof PaperclipCloudConnectorError && e.code === "REAUTHORIZATION_REQUIRED") {
    const session = await connector.startAuthorization({ subject, companyId, profile, returnUri, returnState });
    // redirect user to session.confirmationUrl to grant the new scopes
  } else { throw e; }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// cannot inspect stored scopes without the claim call; detect the mismatch instead
function isScopeMismatch(e: unknown): boolean {
  return e instanceof PaperclipCloudConnectorError && e.code === "REAUTHORIZATION_REQUIRED";
}

Type guard

function isPaperclipCloudConnectorError(e: unknown): e is PaperclipCloudConnectorError {
  return e instanceof PaperclipCloudConnectorError && typeof e.code === "string";
}

Try / catch

try {
  const creds = await connector.claim({ subject, companyId });
} catch (e) {
  if (isPaperclipCloudConnectorError(e) && e.code === "REAUTHORIZATION_REQUIRED") {
    const session = await connector.startAuthorization({ subject, companyId, profile, returnUri, returnState });
    // redirect user to session.confirmationUrl
  } else throw e;
}

Prevention

When it happens

Trigger: openCredentials is called (via claim or refresh) while the stored credentials.scopes set differs from definition.scopes — e.g. the connector definition was updated to add/remove a scope after the user originally authorized, or the broker returned a partial grant at authorization time.

Common situations: A deploy upgraded the connector profile (e.g. gmail.draft gained an extra Gmail scope) while users hold old credentials; a broker-side policy change narrowed the granted scopes; credentials were provisioned by an older connector version with a different scope list.

Related errors


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