import { parseQuery } from "./util.js" ; import { ERR_HASS_HOST_REQUIRED , ERR_INVALID_AUTH , ERR_INVALID_HTTPS_TO_HTTP , } from "./errors.js" ; export type AuthData = { hassUrl : string ; clientId : string | null ; expires : number ; refresh_token : string ; access_token : string ; expires_in : number ; } ; export type SaveTokensFunc = ( data : AuthData | null ) => void ; export type LoadTokensFunc = ( ) => Promise < AuthData | null | undefined > ; export type getAuthOptions = { hassUrl ? : string ; clientId ? : string | null ; redirectUrl ? : string ; authCode ? : string ; saveTokens ? : SaveTokensFunc ; loadTokens ? : LoadTokensFunc ; } ; type QueryCallbackData = { } | { state : string ; code : string ; auth_callback : string ; } ; type OAuthState = { hassUrl : string ; clientId : string | null ; } ; type AuthorizationCodeRequest = { grant_type : "authorization_code" ; code : string ; } ; type RefreshTokenRequest = { grant_type : "refresh_token" ; refresh_token : string ; } ; export const genClientId = ( ) : string => `${ location . protocol }//${ location . host }/` ; export const genExpires = ( expires_in ) : number => { return expires_in * 1000 + Date . now ( ) ; } ; function genRedirectUrl ( ) { const { protocol , host , pathname , search } = location ; return `${ protocol }//${ host }${ pathname }${ search }` ; } function genAuthorizeUrl ( hassUrl , clientId , redirectUrl , state ) { let authorizeUrl = `${ hassUrl }/auth/authorize?response_type=code&redirect_uri=${ encodeURIComponent ( redirectUrl ) }` ; if ( clientId !== null ) { authorizeUrl += `&client_id=${ encodeURIComponent ( clientId ) }` ; } if ( state ) { authorizeUrl += `&state=${ encodeURIComponent ( state ) }` ; } return authorizeUrl ; } function redirectAuthorize ( hassUrl , clientId , redirectUrl , state ) { redirectUrl += ( redirectUrl . includes ( "?" ) ? "&" : "?" ) + "auth_callback=1" ; document . location ! . href = genAuthorizeUrl ( hassUrl , clientId , redirectUrl , state ) ; } async function tokenRequest ( hassUrl , clientId , data ) { const l = typeof location !== "undefined" && location ; if ( l && l . protocol === "https:" ) { const a = document . createElement ( "a" ) ; a . href = hassUrl ; if ( a . protocol === "http:" && a . hostname !== "localhost" ) { throw ERR_INVALID_HTTPS_TO_HTTP ; } } const formData = new FormData ( ) ; if ( clientId !== null ) { formData . append ( "client_id" , clientId ) ; } Object . keys ( data ) . forEach ( ( key ) => { formData . append ( key , data [ key ] ) ; } ) ; const resp = await fetch ( `${ hassUrl }/auth/token` , { method : "POST" , credentials : "same-origin" , body : formData , } ) ; if ( ! resp . ok ) { throw resp . status === 400 || resp . status === 403 ? ERR_INVALID_AUTH : new Error ( "Unable to fetch tokens" ) ; } const tokens = await resp . json ( ) ; tokens . hassUrl = hassUrl ; tokens . clientId = clientId ; tokens . expires = genExpires ( tokens . expires_in ) ; return tokens ; } function fetchToken ( hassUrl , clientId , code ) { return tokenRequest ( hassUrl , clientId , { code , grant_type : "authorization_code" , } ) ; } function encodeOAuthState ( state ) { return btoa ( JSON . stringify ( state ) ) ; } function decodeOAuthState ( encoded ) { return JSON . parse ( atob ( encoded ) ) ; } export class Auth { private _saveTokens ? : SaveTokensFunc ; data : AuthData ; constructor ( data , saveTokens ? ) { this . data = data ; this . _saveTokens = saveTokens ; } get wsUrl ( ) { return `ws${ this . data . hassUrl . substr ( 4 ) }/api/websocket` ; } get accessToken ( ) { return this . data . access_token ; } get expired ( ) { return Date . now ( ) > this . data . expires ; } /**
     * Refresh the access token.
     */ async refreshAccessToken ( ) { if ( ! this . data . refresh_token ) throw new Error ( "No refresh_token" ) ; const data = await tokenRequest ( this . data . hassUrl , this . data . clientId , { grant_type : "refresh_token" , refresh_token : this . data . refresh_token , } ) ; data . refresh_token = this . data . refresh_token ; this . data = data ; if ( this . _saveTokens ) this . _saveTokens ( data ) ; } /**
     * Revoke the refresh & access tokens.
     */ async revoke ( ) { if ( ! this . data . refresh_token ) throw new Error ( "No refresh_token to revoke" ) ; const formData = new FormData ( ) ; formData . append ( "action" , "revoke" ) ; formData . append ( "token" , this . data . refresh_token ) ; await fetch ( `${ this . data . hassUrl }/auth/token` , { method : "POST" , credentials : "same-origin" , body : formData , } ) ; if ( this . _saveTokens ) { this . _saveTokens ( null ) ; } } } export function createLongLivedTokenAuth ( hassUrl , access_token ) { return new Auth ( { hassUrl , clientId : null , expires : Date . now ( ) + 1e11 , refresh_token : "" , access_token , expires_in : 1e11 , } ) ; } export async function getAuth ( options = { } ) { let data ; let hassUrl = options . hassUrl ; if ( hassUrl && hassUrl [ hassUrl . length - 1 ] === "/" ) { hassUrl = hassUrl . substr ( 0 , hassUrl . length - 1 ) ; } const clientId = options . clientId !== undefined ? options . clientId : genClientId ( ) ; if ( ! data && options . authCode && hassUrl ) { data = await fetchToken ( hassUrl , clientId , options . authCode ) ; if ( options . saveTokens ) { options . saveTokens ( data ) ; } } if ( ! data ) { const query = parseQuery < QueryCallbackData > ( location . search . substr ( 1 ) ) ; if ( "auth_callback" in query ) { const state = decodeOAuthState ( query . state ) ; data = await fetchToken ( state . hassUrl , state . clientId , query . code ) ; if ( options . saveTokens ) { options . saveTokens ( data ) ; } } } if ( ! data && options . loadTokens ) { data = await options . loadTokens ( ) ; } if ( data ) { return new Auth ( data , options . saveTokens ) ; } if ( hassUrl === undefined ) { throw ERR_HASS_HOST_REQUIRED ; } redirectAuthorize ( hassUrl , clientId , options . redirectUrl || genRedirectUrl ( ) , encodeOAuthState ( { hassUrl , clientId , } ) ) ; return new Promise < Auth > ( ( ) => { } ) ; }