{"record":{"id":"85440c82a6ecb987","repo":"toeverything/AFFiNE","slug":"32000","errorCode":"-32000","errorMessage":"Authentication failed","messagePattern":"Authentication failed","errorType":"http","errorClass":"UnauthorizedException","httpStatus":401,"severity":"critical","filePath":"packages/backend/server/src/plugins/copilot/mcp/credential.ts","lineNumber":172,"sourceCode":"    if (result.count) {\n      this.event.emit('mcp.credential.revoked', {\n        credentialId: id,\n        userId,\n        workspaceId,\n      });\n    }\n    return result.count > 0;\n  }\n\n  async authenticate(token: string, workspaceId: string) {\n    const parsed = this.parse(token);\n    if (!parsed) throw new UnauthorizedException();\n\n    const credential = await this.models.mcpCredential.authenticate(\n      parsed.id,\n      workspaceId\n    );\n    if (!credential) throw new UnauthorizedException();\n\n    const actualHash = this.crypto.sha256(parsed.secret).toString('hex');\n    if (!this.crypto.compare(actualHash, credential.secretHash)) {\n      throw new UnauthorizedException();\n    }\n\n    const now = new Date();\n    await this.models.mcpCredential.touch(\n      credential.id,\n      new Date(now.getTime() - LAST_USED_WRITE_INTERVAL_MS),\n      now\n    );\n    return credential;\n  }\n\n  private async issue(\n    input: IssueMcpCredential & {\n      familyId?: string;","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/mcp/credential.ts#L154-L190","documentation":"UnauthorizedException in authenticate() when models.mcpCredential.authenticate(parsed.id, workspaceId) returns no row: the token parses fine, but no live credential with that id exists for that workspace (unknown id, wrong workspace, revoked, replaced past grace, or expired). The MCP controller maps it to JSON-RPC -32000 'Authentication failed' with HTTP 401.","triggerScenarios":"Connecting with a token from workspace A against workspace B's MCP endpoint; credential revoked or expired (outside rotation grace); credential deleted; id segment corrupted so the lookup misses.","commonSituations":"Workspace-scoped MCP URL changed after workspace migration; old token kept in client config after rotation dropped grace; credential cleaned up by an admin.","solutions":["Verify the MCP endpoint URL's workspaceId matches the workspace the credential was issued in","If the credential was rotated, use the newest token in the family (old secret dies after graceEndsAt)","If revoked/expired, issue a new credential and update the client config","Check the credential still exists and is active via the GraphQL credential list"],"exampleFix":"// before\nconst mcp = new McpClient({ url: `.../mcp/${otherWorkspaceId}`, token: oldToken });\n\n// after\nconst active = await findActiveCredential(credential.workspaceId);\nconst mcp = new McpClient({\n  url: `.../mcp/${active.workspaceId}`,\n  token: revealedTokenFor(active),\n});","handlingStrategy":"validation","validationCode":"const cred = await models.mcpCredential.get(parsedToken.id);\nconst usable = !!cred && !cred.revokedAt && !cred.replacedById && cred.expiresAt > new Date();\nif (!usable || cred.workspaceId !== urlWorkspaceId) {\n  throw new Error('Credential not active for this workspace — reissue or fix URL');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await mcpClient.connect();\n} catch (e) {\n  if (e.code === -32000) {\n  // id unknown for this workspace: fix workspace mismatch or reissue credential\n    await syncCredentialWithWorkspace();\n    await mcpClient.connect();\n  } else throw e;\n}","preventionTips":["Bind the credential to the same workspaceId that appears in the MCP endpoint URL","After rotation or revocation, sweep all client configs for the old credential id","Treat -32000 on connect as 'credentials/workspace mismatch' and resync rather than retry"],"tags":["mcp","credential","authentication","unauthorized"],"backgroundTag":"invalid-credentials","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}