
    pirQ                     |   d Z ddlZddlZddlZddlZddlZddlZddlZddlm	Z	m
Z
 ddlmZ ddlmZ ddlmZ ddlmZmZmZ ddlZddlZdZd	Zd
ZdZ ee          j        j        j        dz  ZdZ G d d          Z G d d          Z  G d d          Z!e!Z" G d de
          Z# G d d          Z$de$fdZ%dS )a  OAuth2 authentication for WHOOP API with secure token storage.

Supports hybrid storage: macOS Keychain (primary) + file fallback (for cron jobs).

IMPORTANT: WHOOP uses rotating refresh tokens. Each successful refresh returns a NEW
refresh token and invalidates the old one. This means:
- Only one process should refresh at a time (file locking)
- Tokens must be saved immediately after refresh
- If refresh fails with 400, the refresh token is likely invalid and re-auth is needed
    N)
HTTPServerBaseHTTPRequestHandler)Path)Thread)Optional)	urlencodeparse_qsurlparse   z,https://api.prod.whoop.com/oauth/oauth2/authz-https://api.prod.whoop.com/oauth/oauth2/tokenz	whoop-mcpz.tokens.jsonz9read:recovery read:sleep read:cycles read:workout offlinec            	       z    e Zd ZdZededededefd            Zede	e
         fd            Zed
d	            ZdS )KeychainTokenStoragez*Secure token storage using macOS Keychain.access_tokenrefresh_token
expires_atreturnc                     	 t          j        | ||d          }t          j        t          d|           dS # t
          $ r}t          d|            Y d}~dS d}~ww xY w)zAStore OAuth tokens securely in keychain. Returns True on success.r   r   r   tokensTzKeychain storage failed: NF)jsondumpskeyringset_passwordSERVICE_NAME	Exceptionprint)r   r   r   
token_dataes        j/Users/kimhansen/Desktop/03 Workspace/ceo-agents/chl-effectiveness/mcp-servers/whoop/src/whoop_mcp/auth.pystore_tokensz!KeychainTokenStorage.store_tokens0   s    
	 ,!.(% %  J
  xDDD4 	 	 	1a1122255555	s   37 
AAAc                      	 t          j        t          d          } | rt          j        |           S n# t
          $ r
}Y d}~nd}~ww xY wdS )zRetrieve tokens from keychain.r   N)r   get_passwordr   r   loadsr   )r   r   s     r   
get_tokenszKeychainTokenStorage.get_tokens?   sg    	 -lHEEJ .z*---. 	 	 	DDDD	 ts   /3 
AANc                      	 t          j        t          d           dS # t           j        j        $ r Y dS t
          $ r Y dS w xY w)zRemove tokens from keychain.r   N)r   delete_passwordr   errorsPasswordDeleteErrorr        r   clear_tokensz!KeychainTokenStorage.clear_tokensK   s`    	#L(;;;;;~1 	 	 	DD 	 	 	DD	s    A	AAr   N__name__
__module____qualname____doc__staticmethodstrfloatboolr   r   dictr#   r*   r(   r)   r   r   r   -   s        443 s  RV    \ 	 	 	 	 \	    \  r)   r   c                       e Zd ZdZeddededededef
d            Zede	e
         fd	            Zedd
            ZdS )FileTokenStoragea  File-based token storage for non-GUI contexts (cron jobs).

    Stores tokens in a JSON file with 600 permissions for security.
    This is a fallback when keychain is not accessible.
    Uses file locking to prevent race conditions with rotating refresh tokens.
    Nr   r   r   refreshed_atr   c                    	 | |||pt          j                     d}t          t          t                    dz             }t	          |d          5 }t          j        |                                t
          j                   	 t          t          t                    dz             }|	                    t          j        |d                     t          j        |t          j        t          j        z             |                    t                     t          j        |                                t
          j                   n6# t          j        |                                t
          j                   w xY w	 ddd           n# 1 swxY w Y   dS # t&          $ r}t)          d	|            Y d}~d
S d}~ww xY w)zFStore OAuth tokens in file with file locking. Returns True on success.r   r   r   r8   .lockwz.tmp   )indentNTzFile storage failed: F)timer   r2   
TOKEN_FILEopenfcntlflockfilenoLOCK_EX
write_textr   r   oschmodstatS_IRUSRS_IWUSRrenameLOCK_UNr   r   )	r   r   r   r8   r   	lock_filelf	temp_filer   s	            r   r   zFileTokenStorage.store_tokens^   s   	 ,!.( , ;		 J S__w677Ii%% 	<BIIKK777< $S__v%= > >I((Jq)I)I)IJJJHYt|(CDDD$$Z000K		U];;;;EK		U];;;;;	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 4 	 	 	-!--...55555	sU   AF 2FBE2F3E;;F?F FF FF 
F?"F::F?c                  ~   	 t                                           rt          t          t                     dz             } t	          | d          5 }t          j        |                                t
          j                   	 t          j
        t                                                     t          j        |                                t
          j                   cddd           S # t          j        |                                t
          j                   w xY w# 1 swxY w Y   n# t          $ r Y nw xY wdS )z,Retrieve tokens from file with file locking.r;   r<   N)r@   existsr   r2   rA   rB   rC   rD   LOCK_SHr   r"   	read_textrM   r   )rN   rO   s     r   r#   zFileTokenStorage.get_tokens{   sP   
	  "" @ Z7!:;;	)S)) @RK		U];;;@#z**>*>*@*@AABIIKK???@ @ @ @ @ @ @ @
 BIIKK????@ @ @ @ @ @ @ @  	 	 	D	tsN   AD- 2D!*C+-1D!D- +3DD!!D%%D- (D%)D- -
D:9D:c                      	 t                                           rt                                            dS dS # t          $ r Y dS w xY w)zRemove token file.N)r@   rR   unlinkr   r(   r)   r   r*   zFileTokenStorage.clear_tokens   s_    	  "" $!!#####$ $ 	 	 	DD	s   28 
AANr+   r,   r(   r)   r   r7   r7   V   s           3 s  ]b nr    \8     \    \  r)   r7   c                   $   e Zd ZdZeddededededdf
d            Zedee	         fd	            Z
edd
            Zedde	dedefd            Zede	defd            Zedde	dedefd            Zedefd            Zede	fd            ZdS )HybridTokenStoragea
  Hybrid token storage: tries keychain first, falls back to file.

    - GUI contexts (MCP server in terminal): Keychain works, file is backup
    - Non-GUI contexts (cron jobs): Keychain fails, file is used

    When storing, writes to BOTH to keep them in sync.
    Nr   r   r   r8   r   c                     |pt          j                     }t                              | ||          }t                              | |||          }|s|st	          d          dS dS )z'Store tokens in both keychain and file.z0Failed to store tokens in both keychain and fileN)r?   r   r   r7   RuntimeError)r   r   r   r8   keychain_okfile_oks         r   r   zHybridTokenStorage.store_tokens   s~     $2ty{{*77mU_``"//mZYeff 	S7 	SQRRR	S 	S 	S 	Sr)   c                  n    t                                           } | r| S t                                          S )z,Get tokens, trying keychain first then file.)r   r#   r7   r   s    r   r#   zHybridTokenStorage.get_tokens   s6     &0022 	M  **,,,r)   c                  j    t                                            t                                           dS )z)Clear tokens from both storage locations.N)r   r*   r7   r(   r)   r   r*   zHybridTokenStorage.clear_tokens   s.     	))+++%%'''''r)   ,  r   buffer_secondsc                 b    |                      dd          }t          j                    ||z
  k    S )z5Check if access token is expired (with 5 min buffer).r   r   getr?   r   rb   r   s      r   is_token_expiredz#HybridTokenStorage.is_token_expired   -     ZZa00
y{{j>9::r)   c                     |                      d|                      dd          dz
            }t          j                    |z
  dz  }|t          k    S )zCheck if refresh token is likely expired (based on when we last refreshed).

        WHOOP refresh tokens have a limited lifetime (~7 days). If we haven't
        refreshed in that time, the refresh token is probably invalid.
        r8   r   r     iQ )re   r?   REFRESH_TOKEN_LIFETIME_DAYS)r   r8   days_since_refreshs      r   is_refresh_token_likely_expiredz2HybridTokenStorage.is_refresh_token_likely_expired   sM     zz.&**\12M2MPT2TUU"ikkL8YG!$???r)   X  c                 b    |                      dd          }t          j                    ||z
  k    S )z>Check if we should proactively refresh (10 min before expiry).r   r   rd   rf   s      r   should_proactively_refreshz-HybridTokenStorage.should_proactively_refresh   rh   r)   c                      t                                           } | r.t                              | d         | d         | d                   S dS )z?Copy tokens from keychain to file (for setting up cron access).r   r   r   F)r   r#   r7   r   r_   s    r   sync_keychain_to_filez(HybridTokenStorage.sync_keychain_to_file   sT     &0022 	#00~&'|$  
 ur)   c                     t                                           } t                                          }| du| r|                     d          ndd|dut	          t
                    |r|                    d          ndddS )z6Get status of both storage backends (for diagnostics).Nr   )	availabler   )rt   pathr   )keychainfile)r   r#   r7   re   r2   r@   )keychain_tokensfile_tokenss     r   get_storage_statusz%HybridTokenStorage.get_storage_status   s     /99;;&1133 -D8CR\o11,???X\ 
 )4J?JTkool;;;PT 

 

 
	
r)   rW   r+   )ra   )rn   )r-   r.   r/   r0   r1   r2   r3   r   r   r5   r#   r*   intr4   rg   rm   rp   rr   rz   r(   r)   r   rY   rY      s         S S3 Ss S S]b Snr S S S \S - - - - \- ( ( ( \(
 ; ; ;s ;T ; ; ; \;
 @ @ @ @ @ \@ ; ;4 ; ;t ; ; ; \;
 	4 	 	 	 \	 
 
 
 
 \
 
 
r)   rY   c                   z    e Zd ZU dZdZee         ed<   dZee         ed<   dZ	ee         ed<   d Z
defdZd	 ZdS )
OAuthCallbackHandlerz HTTP handler for OAuth callback.Nauthorization_codestateerrorc                    t          | j                  }t          |j                  }d|v r/|d         d         t          _        |                     d           dS d|v rV|d         d         t          _        |                    ddg          d         t          _	        |                     d           dS |                     d           dS )	z"Handle OAuth callback GET request.r   r   z0Authorization failed. You can close this window.coder   Nz4Authorization successful! You can close this window.z-Invalid callback. Missing authorization code.)
r
   ru   r	   queryr}   r   _send_responser~   re   r   )selfparsedparamss      r   do_GETzOAuthCallbackHandler.do_GET   s    $)$$&,''f)/); & RSSSSSv6<VnQ6G 3)/GdV)D)DQ)G & VWWWWW OPPPPPr)   messagec                     |                      d           |                     dd           |                                  d| d}| j                            |                                           dS )zSend HTML response.   zContent-typez	text/htmlz
        <!DOCTYPE html>
        <html>
        <head><title>WHOOP Authorization</title></head>
        <body style="font-family: system-ui; text-align: center; padding: 50px;">
            <h1>z.</h1>
        </body>
        </html>
        N)send_responsesend_headerend_headerswfilewriteencode)r   r   htmls      r   r   z#OAuthCallbackHandler._send_response	  s    3555
    	
'''''r)   c                     dS )zSuppress HTTP server logs.Nr(   )r   formatargss      r   log_messagez OAuthCallbackHandler.log_message  s    r)   )r-   r.   r/   r0   r~   r   r2   __annotations__r   r   r   r   r   r(   r)   r   r}   r}      s         **(,,,,E8C=E8C=Q Q Q(c ( ( ( (     r)   r}   c                       e Zd ZdZddededefdZedej        fd            Z	de
e         fd	Zddedede
e         fdZdefdZdedefdZdefdZddZdS )	WhoopAuthz$WHOOP OAuth2 authentication handler.http://localhost:8765/callback	client_idclient_secretredirect_uric                 >    || _         || _        || _        d | _        d S rW   )r   r   r   _http_client)r   r   r   r   s       r   __init__zWhoopAuth.__init__!  s'    "*(48r)   r   c                 R    | j         t          j        d          | _         | j         S )zLazy-load HTTP client.Ng      >@timeout)r   httpxClientr   s    r   http_clientzWhoopAuth.http_client'  s+     $ %T : : :D  r)   c                 6   t                                           }|sdS t                               |          rMt          dt           d           t          d           t                               |d          s|d         S dS t                               |          st                               |          rZ|                     |d                   }|r|d         S t                               |d          st          d	           |d         S dS |d         S )
zGet a valid access token, refreshing if necessary.

        Uses proactive refresh (before expiry) to reduce failure risk.
        Nz(WARNING: Refresh token likely expired (>z days old).z%Run setup_auth.py to re-authenticate.r   rb   r   r   z7Refresh failed but current token still valid, using it.)TokenStorager#   rm   r   rk   rg   rp   _refresh_tokens)r   r   
new_tokenss      r   get_valid_access_tokenz WhoopAuth.get_valid_access_token.  s-   
 ((** 	4 77?? 	e=Xeeefff9:::000JJ .n--4 226:: 	l>[>[\b>c>c 	--f_.EFFJ 2!.11000JJ .OPPPn--4n%%r)   r   r   retry_countc           	      t	   t          t          t                    dz             }	 t          |d          5 }t	          j        |                                t          j        t          j        z             	 t          
                                }|r[t                              |d          s?|t	          j        |                                t          j                   cddd           S |r|                    d|          n|}| j                            t           d|| j        | j        ddd	i
          }|j        dk    r|j        r|                                ni }|                    dd          }	t-          d|                    dd                      dt          |                                          v rdt-          d           t-          d           t-          d           t-          d           t-          d           t                                           	 t	          j        |                                t          j                   ddd           dS |                                 |                                }
t5          j                    |
                    dd          z   }t5          j                    }t                              |
d         |
                    d|          ||           t          
                                t	          j        |                                t          j                   cddd           S # t	          j        |                                t          j                   w xY w# 1 swxY w Y   dS # t8          $ r |dk     ryt-          d           t5          j        d           t          
                                }|r t                              |d          s|cY S |                     ||dz             cY S t-          d           Y dS t>          j         $ rO}t-          d |j!        j                    |j!        j        rt-          d!|j!        j                    Y d}~dS d}~wtD          $ r}t-          d"|            Y d}~dS d}~ww xY w)#a  Refresh the access token using refresh token.

        IMPORTANT: WHOOP uses rotating refresh tokens. Each successful refresh
        returns a NEW refresh token and invalidates the old one. We must:
        1. Use file locking to prevent concurrent refreshes
        2. Save the new tokens immediately
        3. Handle 400 errors (invalid refresh token) by prompting re-auth
        z.refresh.lockr<   <   r   Nr   )
grant_typer   r   r   Content-Type!application/x-www-form-urlencodeddataheadersi  
error_hint z
Token refresh failed (400): error_descriptionzUnknown errorinvalidz1
The refresh token is invalid. This happens when:z4  1. Another process already used this refresh tokenz(  2. The refresh token expired (~7 days)z  3. WHOOP revoked the tokenz'
Run setup_auth.py to re-authenticate.

expires_inrj   r   r:      z0Another process is refreshing tokens, waiting...r=      z&Timeout waiting for token refresh lockzToken refresh HTTP error: z
Response: zToken refresh failed: )#r   r2   r@   rA   rB   rC   rD   rE   LOCK_NBr   r#   rg   rM   re   r   post	TOKEN_URLr   r   status_codetextr   r   lowerr*   raise_for_statusr?   r   BlockingIOErrorsleepr   r   HTTPStatusErrorresponser   )r   r   r   rN   rO   current_tokenscurrent_refreshr   
error_datar   r   r   r8   r   r   s                  r   r   zWhoopAuth._refresh_tokensN  s    Z?:;;	O	i%% 6<BIIKK)FGGG4<%1%<%<%>%>N% .l.K.KNkm.K.n.n .-^ K		U];;;m6< 6< 6< 6< 6< 6< 6< 6< ]k&}n&8&8-&X&X&Xp}O#/44!*9-<)--1-?	  "01T U  5 	  	 H  +s228@%MX]]___2
%/^^L"%E%E
uz~~Nacr?s?suuvvv$J(=(=(?(???!"VWWW!"XYYY!"LMMM!"@AAA!"MNNN(55777#& K		U];;;m6< 6< 6< 6< 6< 6< 6< 6<J --///#==??D!%txxd/K/K!KJ#'9;;L !--%).%9&*hh&P&P#-%1	 .    (2244 K		U];;;m6< 6< 6< 6< 6< 6< 6< 6<l K		U];;;;m6< 6< 6< 6< 6< 6< 6< 6< 6< 6<p  	 	 	QHIII
1%0022 ","?"?WY"?"Z"Z "!MMM++M;?KKKKK:;;;44$ 	 	 	Gqz/EGGHHHz 641:?4455544444 	 	 	.1..///44444	s   N ?N68M.1NN ,D9M&1NN $B5M1N
N 3N

NNN NN A,R7R7#R75R7ARR7R22R7c                 Z   t          j        d          }d| j        | j        t          |d}t
           dt          |           }t          | j                  }|j        pd}dt          _
        dt          _        dt          _        t          d|ft                    }t          |j                  }|                                 t#          d	           t#          d
| d           t%          j        |           |                    d           |                                 t          j        rt#          dt          j                    dS t          j
        st#          d           dS t          j        |k    rt#          d           dS |                     t          j
                  S )z/Start OAuth authorization flow (opens browser).    r   )response_typer   r   scoper   ?i="  N	localhost)targetz+
Opening browser for WHOOP authorization...z If browser doesn't open, visit: 
x   r   zAuthorization error: FzNo authorization code receivedz%State mismatch - possible CSRF attack)secretstoken_urlsafer   r   SCOPESAUTH_URLr   r
   portr}   r~   r   r   r   r   handle_requeststartr   
webbrowserrA   joinserver_close_exchange_code_for_tokens)r   r   auth_paramsauth_urlr   r   serverserver_threads           r   start_authorization_flowz"WhoopAuth.start_authorization_flow  s   %b)) $ -
 
 99;!7!799 $+,,{"d 37/%)"%)" [$/1EFFf&;<<< 	=>>>====>>>!!! 	3'''% 	F*>*DFFGGG5#6 	23335%..9:::5 --.B.UVVVr)   r   c           	         	 | j                             t          d|| j        | j        | j        dddi          }|                                 |                                }t          j                    |	                    dd          z   }t                              |d         |d	         |
           t          d           dS # t          $ r}t          d|            Y d}~dS d}~ww xY w)z.Exchange authorization code for access tokens.r~   )r   r   r   r   r   r   r   r   r   rj   r   r   r   z=Authorization successful! Tokens stored in keychain and file.TzToken exchange failed: NF)r   r   r   r   r   r   r   r   r?   re   r   r   r   r   )r   r   r   r   r   r   s         r   r   z#WhoopAuth._exchange_code_for_tokens  s   	',,"6 $($5!%%)%7  ()LM - 
 
H %%'''==??Dtxxd'C'CCJ%%!.1"?3% &    QRRR4 	 	 	/A//00055555	s   CC 
C.C))C.c                 .    |                                  duS )z%Check if we have valid authorization.N)r   r   s    r   is_authorizedzWhoopAuth.is_authorized  s    **,,D88r)   Nc                 V    t                                            t          d           dS )zClear stored tokens.z&Authorization revoked. Tokens cleared.N)r   r*   r   r   s    r   revokezWhoopAuth.revoke  s)    !!###677777r)   )r   )r   r+   )r-   r.   r/   r0   r2   r   propertyr   r   r   r   r   r{   r5   r   r4   r   r   r   r   r(   r)   r   r   r     s?       ..9 9# 9c 9 9 9 9 9 !U\ ! ! ! X!& & & & &@Z ZS Zs Z8TX> Z Z Z Zx2W$ 2W 2W 2W 2Whc d    >9t 9 9 9 98 8 8 8 8 8r)   r   r   c                  n   ddl m}  t          j                            t          j                            t                    ddd          } | |           t          j        d          }t          j        d          }t          j        dd          }|r|st          d	          t          |||          S )
z5Create WhoopAuth instance from environment variables.r   )load_dotenvz..z.envWHOOP_CLIENT_IDWHOOP_CLIENT_SECRETWHOOP_REDIRECT_URIr   zcMissing WHOOP credentials. Set WHOOP_CLIENT_ID and WHOOP_CLIENT_SECRET in environment or .env file.)
dotenvr   rG   ru   r   dirname__file__getenv
ValueErrorr   )r   env_pathr   r   r   s        r   get_auth_from_envr     s    """""" w||BGOOH55tT6JJHK	+,,II344M913STTL 
M 
+
 
 	

 Y|<<<r)   )&r0   rB   r   rG   r   rI   r?   r   http.serverr   r   pathlibr   	threadingr   typingr   urllib.parser   r	   r
   r   r   rk   r   r   r   r   parentr@   r   r   r7   rY   r   r}   r   r   r(   r)   r   <module>r      s  	 	   				        : : : : : : : :                   6 6 6 6 6 6 6 6 6 6      :;	  T(^^")0>A
 
E& & & & & & & &R< < < < < < < <~W
 W
 W
 W
 W
 W
 W
 W
v "( ( ( ( (1 ( ( (Vf8 f8 f8 f8 f8 f8 f8 f8R=9 = = = = = =r)   