{"record":{"id":"cc266580e031f607","repo":"paperclipai/paperclip","slug":"reauthorization-required","errorCode":"REAUTHORIZATION_REQUIRED","errorMessage":"Paperclip Cloud scope grant did not match","messagePattern":"Paperclip Cloud scope grant did not match","errorType":"error_code","errorClass":"PaperclipCloudConnectorError","httpStatus":null,"severity":"error","filePath":"server/src/services/paperclip-cloud-connector.ts","lineNumber":309,"sourceCode":"      config.environment,\n      definition.provider,\n      profile,\n      definition.scopes,\n    );\n    if (\n      credentials.instanceId !== config.instanceId\n      || credentials.environment !== config.environment\n      || credentials.subject !== subject\n      || credentials.companyId !== companyId\n      || credentials.provider !== definition.provider\n    ) {\n      throw new PaperclipCloudConnectorError(\"Paperclip Cloud credential binding did not match\", \"CONNECTOR_BINDING_MISMATCH\");\n    }\n    if (credentials.profile !== profile) {\n      throw new PaperclipCloudConnectorError(\"Paperclip Cloud connector profile binding did not match\", \"CONNECTOR_BINDING_MISMATCH\");\n    }\n    if (!sameStringSet(credentials.scopes, definition.scopes)) {\n      throw new PaperclipCloudConnectorError(\"Paperclip Cloud scope grant did not match\", \"REAUTHORIZATION_REQUIRED\");\n    }\n    return credentials;\n  }\n\n  return {\n    async getInstanceStatus(): Promise<\"active\" | \"suspended\" | \"removed\"> {\n      let response: ConnectorResponse;\n      try {\n        response = await call(\"status\", {\n          subject: \"instance-status\",\n          companyId: \"instance-status\",\n        });\n      } catch (error) {\n        if (error instanceof PaperclipCloudConnectorError && error.status === 401) return \"removed\";\n        throw error;\n      }\n      if (response.status === \"active\" && response.active === true) return \"active\";\n      if (response.status === \"suspended\" && response.active === false) return \"suspended\";","sourceCodeStart":291,"sourceCodeEnd":327,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/paperclip-cloud-connector.ts#L291-L327","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-run the authorization flow (startAuthorization) so the user grants the current definition scopes","Diff credentials.scopes against definition.scopes to identify which scope is missing/extra, then update the connector definition or re-consent","If the definition change was unintentional, revert the scope list in the connector definition so it matches existing grants","Verify the broker is returning the full requested scope set and not a reduced one"],"exampleFix":"// before (stale grant after definition change)\nconst creds = await connector.claim({ subject, companyId }); // throws REAUTHORIZATION_REQUIRED\n\n// after (detect and re-authorize)\ntry {\n  const creds = await connector.claim({ subject, companyId });\n} catch (e) {\n  if (e instanceof PaperclipCloudConnectorError && e.code === \"REAUTHORIZATION_REQUIRED\") {\n    const session = await connector.startAuthorization({ subject, companyId, profile, returnUri, returnState });\n    // redirect user to session.confirmationUrl to grant the new scopes\n  } else { throw e; }\n}","handlingStrategy":"try-catch","validationCode":"// cannot inspect stored scopes without the claim call; detect the mismatch instead\nfunction isScopeMismatch(e: unknown): boolean {\n  return e instanceof PaperclipCloudConnectorError && e.code === \"REAUTHORIZATION_REQUIRED\";\n}","typeGuard":"function isPaperclipCloudConnectorError(e: unknown): e is PaperclipCloudConnectorError {\n  return e instanceof PaperclipCloudConnectorError && typeof e.code === \"string\";\n}","tryCatchPattern":"try {\n  const creds = await connector.claim({ subject, companyId });\n} catch (e) {\n  if (isPaperclipCloudConnectorError(e) && e.code === \"REAUTHORIZATION_REQUIRED\") {\n    const session = await connector.startAuthorization({ subject, companyId, profile, returnUri, returnState });\n    // redirect user to session.confirmationUrl\n  } else throw e;\n}","preventionTips":["Keep connector definition scopes stable; bump them only with a planned re-consent migration","On deploy, diff definition.scopes against previously granted scopes and notify users to re-authorize","Prefer additive scope changes and request the superset up front"],"tags":["oauth","scopes","reauthorization","connector"],"backgroundTag":"oauth-token-expired","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}