import Logger from "bunyan" ; import axios , { AxiosError , AxiosInstance } from "axios" ; import url from "url" ; import statsd from "../../config/statsd" ; import { getLogger } from "../../config/logger" ; import { metricHttpRequest } from "../../config/metric-names" ; import { createQueryStringHash , encodeSymmetric } from "atlassian-jwt" ; import { urlParamsMiddleware } from "../../util/axios/common-middleware" ; const instance = process . env . INSTANCE_NAME ; const iss = `com.github.integration${ instance ? `.${ instance }` : "" }` ; declare module "axios" { interface AxiosRequestConfig { urlParams ? : Record < string , string > ; } } param { string } secret function getAuthMiddleware ( secret ) { return ( param { import ( "axios" ) . AxiosRequestConfig } config returns { import ( "axios" ) . AxiosRequestConfig } ( config ) => { const { query , pathname } = url . parse ( config . url , true ) ; const jwtToken = encodeSymmetric ( { ... getExpirationInSeconds ( ) , iss , qsh : createQueryStringHash ( { method : config . method , pathname : pathname || undefined , query } ) } , secret ) ; return { ... config , headers : { ... config . headers , Authorization : `JWT ${ jwtToken }` } } ; } ) ; } /**
 * Wrapper for AxiosError, which includes error status
 */ export class JiraClientError extends Error { status ? : number ; cause : AxiosError ; constructor ( message , cause , status ? ) { super ( message ) ; this . status = status ; this . cause = { ... cause , config : { } } ; } } export const getJiraErrorMessages = ( status ) => { switch ( status ) { case 400 : return "HTTP 400 - Request had incorrect format." ; case 401 : return "HTTP 401 - Missing a JWT token, or token is invalid." ; case 403 : return "HTTP 403 - The JWT token used does not correspond to an app that defines the jiraDevelopmentTool module, or the app does not define the 'WRITE' scope" ; case 413 : return "HTTP 413 - Data is too large. Submit fewer devinfo entities in each payload." ; case 429 : return "HTTP 429 - API rate limit has been exceeded." ; default : return `HTTP ${ status }` ; } } ; /**
 * Middleware to enhance failed requests in Jira.
 */ function getErrorMiddleware ( logger ) { return ( param { import ( "axios" ) . AxiosError } error returns { Promise < Error > } ( error ) : Promise < Error > => { const status = error ?. response ?. status ; const errorMessage = "Error executing Axios Request " + ( status ? getJiraErrorMessages ( status ) : error . message || "" ) ; const isWarning = status && ( status >= 300 && status < 500 && status !== 400 ) ; if ( isWarning ) { logger . warn ( errorMessage ) ; } else { logger . error ( { err : error } , errorMessage ) ; } return Promise . reject ( new JiraClientError ( errorMessage , error , status ) ) ; } ) ; } param { import ( "probot" ) . Logger } logger function getSuccessMiddleware ( logger ) { return ( param { import ( "axios" ) . AxiosResponse } response returns { import ( "axios" ) . AxiosResponse } ( response ) => { logger . debug ( { params : response . config . urlParams } , `Jira request: ${ response . config . method . toUpperCase ( ) } ${ response . config . originalUrl } - ${ response . status } ${ response . statusText }
				Response data: ${ JSON . stringify ( response . data ) }` ) ; return response ; } ) ; } function getExpirationInSeconds ( ) { const nowInSeconds = Math . floor ( Date . now ( ) / 1000 ) ; return { iat : nowInSeconds , exp : nowInSeconds + 30 } ; } param { import ( "axios" ) . AxiosRequestConfig } config returns { import ( "axios" ) . AxiosRequestConfig } const setRequestStartTime = ( config ) => { config . requestStartTime = new Date ( ) ; return config ; } ; /**
 * Extract the path name from a URL.
 *
 */ export const extractPath = ( someUrl = "" ) : string => url . parse ( someUrl ) . pathname || "" ; param { import ( "axios" ) . AxiosResponse } response returns { import ( "axios" ) . AxiosResponse } const instrumentRequest = ( response ) => { if ( ! response ) { return ; } const requestDurationMs = Number ( Date . now ( ) - ( response . config ?. requestStartTime || 0 ) ) ; const tags = { method : response . config ?. method ?. toUpperCase ( ) , path : extractPath ( response . config ?. originalUrl ) , status : response . status } ; statsd . histogram ( metricHttpRequest . jira , requestDurationMs , tags ) ; return response ; } ; param { import ( "axios" ) . AxiosError } error returns { Promise < Error > } const instrumentFailedRequest = ( ) => { return ( error ) => { instrumentRequest ( error ?. response ) ; return Promise . reject ( error ) ; } ; } ; /**
 * Atlassian API JWTs need to be generated per-request due to their use of
 * Query String Hashing (QSH) to prevent URL tampering. Unlike traditional JWTs,
 * QSH requires us to re-encode a JWT for each URL we request to. As a result,
 * it makes sense for us to simply create a new JWT for each request rather than
 * attempt to reuse them. This accomplished using Axios interceptors to
 * just-in-time add the token to a request before sending it.
 */ export default ( baseURL , secret , logger ? ) : AxiosInstance => { logger = logger || getLogger ( "jira.client.axios" ) ; const instance = axios . create ( { baseURL , timeout : Number ( process . env . JIRA_TIMEOUT ) || 30000 } ) ; instance . interceptors . request . use ( setRequestStartTime ) ; instance . interceptors . response . use ( instrumentRequest , instrumentFailedRequest ( ) ) ; instance . interceptors . request . use ( getAuthMiddleware ( secret ) ) ; instance . interceptors . request . use ( urlParamsMiddleware ) ; instance . interceptors . response . use ( getSuccessMiddleware ( logger ) , getErrorMiddleware ( logger ) ) ; return instance ; } ;