diff --git a/backend/apps/tools_lib/tools_lib.py b/backend/apps/tools_lib/tools_lib.py index 7113aa32..3782e6fd 100644 --- a/backend/apps/tools_lib/tools_lib.py +++ b/backend/apps/tools_lib/tools_lib.py @@ -1752,15 +1752,36 @@ async def spotify_callback(code: str = "", state: str = "", error: str = "") -> if not refresh_token: return HTMLResponse("
Retry from OpenSwarm.
", status_code=400) - # Log what Spotify ACTUALLY granted vs what we requested. If a user - # complains about "scope not granted" later, this is the first place - # to check — the granted set should equal _SPOTIFY_SCOPES. + # Spotify returns the actual granted scopes in the token response. + # Compare against what we asked for and bail HARD if any are missing — + # otherwise the user thinks they're connected but every playlist / + # library mutation will 403 later with a cryptic error. granted_scopes = data.get("scope", "") requested_set = set(_SPOTIFY_SCOPES.split()) granted_set = set(granted_scopes.split()) missing = requested_set - granted_set if missing: - logger.warning(f"spotify: Spotify granted fewer scopes than requested. Missing: {missing}") + logger.warning(f"spotify: Spotify granted fewer scopes than requested. Missing: {sorted(missing)}") + missing_pretty = ", ".join(sorted(missing)) + return HTMLResponse( + "" + "Spotify only granted {len(granted_set)} of {len(requested_set)} scopes. " + f"Missing:
" + f"{html.escape(missing_pretty)}"
+ "Your token was NOT saved. The Spotify tile in OpenSwarm is still disconnected.
" + "You can close this tab and return to OpenSwarm.
" + f"All {len(granted_set)} permissions granted, including playlist write access.
" + "You can close this tab and return to OpenSwarm.
" "