NpmDsc.psm1

using namespace System.Collections.Generic

#region Functions
function Assert-Npm {
    # Refresh session $path value before invoking 'npm'
    $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
    try {
        Invoke-Npm -Command 'help'
        return
    } catch {
        throw 'NodeJS is not installed'
    }
}

function Invoke-Npm {
    param (
        [Parameter(Mandatory = $true)]
        [string[]]$Command
    )

    $argList = @($Command | Where-Object { -not [string]::IsNullOrEmpty($_) })
    $value = & npm @argList

    if ($LASTEXITCODE -ne 0) {
        $errors = Get-NpmErrorMessages -LogPath (GetNpmPath)
        throw "Command 'npm $($argList -join ' ')' failed: $($errors -join '; ')"
    }

    return $value
}

function Set-PackageDirectory {
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackageDirectory
    )

    if (Test-Path -Path $PackageDirectory -PathType Container) {
        Set-Location -Path $PackageDirectory
    } else {
        throw "$($PackageDirectory) does not point to a valid directory."
    }
}

function Get-InstalledNpmPackages {
    param (
        [Parameter()]
        [bool]$Global
    )

    $command = [List[string]]::new()
    $command.Add('list')
    $command.Add('--json')

    if ($Global) {
        $command.Add('-g')
    }

    return Invoke-Npm -Command $command
}

function Install-NpmPackage {
    param (
        [Parameter()]
        [string]$PackageName,

        [Parameter()]
        [bool]$Global,

        [Parameter()]
        [string[]]$Arguments
    )

    $command = [List[string]]::new()
    $command.Add('install')
    if (-not [string]::IsNullOrEmpty($PackageName)) {
        $command.Add($PackageName)
    }

    if ($Global) {
        $command.Add('-g')
    }

    foreach ($a in ($Arguments | Where-Object { $_ })) {
        $command.Add($a)
    }

    Write-Verbose -Message "Executing 'npm $($command -join ' ')'"

    return Invoke-Npm -Command $command
}

function Uninstall-NpmPackage {
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackageName,

        [Parameter()]
        [bool]$Global,

        [Parameter()]
        [string[]]$Arguments
    )

    $command = [List[string]]::new()
    $command.Add('uninstall')
    $command.Add($PackageName)

    if ($Global) {
        $command.Add('-g')
    }

    foreach ($a in ($Arguments | Where-Object { $_ })) {
        $command.Add($a)
    }

    Write-Verbose -Message "Executing 'npm $($command -join ' ')'"

    return Invoke-Npm -Command $command
}

function GetNpmPath {
    if ($IsWindows) {
        $npmCacheDir = Join-Path $env:LOCALAPPDATA 'npm-cache' '_logs'
        $globalNpmCacheDir = Join-Path $env:SystemDrive 'npm' 'cache' '_logs'
        if (Test-Path $npmCacheDir -ErrorAction SilentlyContinue) {
            return $npmCacheDir
        } elseif (Test-Path $globalNpmCacheDir -ErrorAction SilentlyContinue) {
            return $globalNpmCacheDir
        } else {
            # Call 'npm' directly rather than through Invoke-Npm: this is an error-reporting helper
            # and Invoke-Npm's failure path calls back into GetNpmPath, which would recurse indefinitely.
            $cacheRoot = (& npm config list --json --logs-max=0 2>$null | ConvertFrom-Json -ErrorAction SilentlyContinue).cache
            $result = if ($cacheRoot) { Join-Path $cacheRoot '_logs' } else { $null }
            if ($result -and (Test-Path $result -ErrorAction SilentlyContinue)) {
                return $result
            } else {
                return $null
            }
        }
    } elseif ($IsLinux -or $IsMacOS) {
        $npmCacheDir = Join-Path $env:HOME '.npm/_logs'
        if (Test-Path $npmCacheDir -ErrorAction SilentlyContinue) {
            return $npmCacheDir
        } else {
            return $null
        }
    } else {
        throw 'Unsupported platform'
    }
}

function GetNpmWhatIfResponse {
    $npmPath = GetNpmPath
    if ($null -ne $npmPath) {
        return (Get-NpmErrorMessages -LogPath $npmPath)
    } else {
        return @('No what-if response found.')
    }
}

function Get-NpmErrorMessages {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$LogPath
    )

    $lastLog = (Get-ChildItem $LogPath -Filter '*.log' | Sort-Object LastWriteTime -Descending)[0]

    Write-Verbose -Message "Found logging cache entry: $($lastLog.FullName)"

    $errorMessages = @()
    if ($lastLog) {
        $logContent = Get-Content $lastLog.FullName
        $regex = [regex]::new('^error\s.*')

        foreach ($line in $logContent) {
            $lineRemovePattern = '^\d+\s*'

            $cleanedLine = $line -replace $lineRemovePattern, ''
            if ($regex.Matches($cleanedLine)) {
                $errorMessages += $cleanedLine
            }
        }

        if ([string]::IsNullOrEmpty($errorMessages)) {
            $errorMessages = @('No what-if response found.')
        }

        return $errorMessages
    }
}
#endRegion Functions

#region Enums
enum Ensure {
    Absent
    Present
}
#endRegion Enums

#region DSCResources
<#
    .SYNOPSIS
        The `NpmInstall` DSC resource is used to install all npm packages listed in a `package.json` file.
 
    .DESCRIPTION
        The `NpmInstall` DSC resource invokes `npm install` to install all packages defined in a
        `package.json` file in the specified directory or globally. It is inherently idempotent
        as npm will resolve all package dependencies on each run.
 
        ## Requirements
 
        * Target machine must have Node.js and npm installed.
 
    .PARAMETER SID
        The security identifier. This is a key property and should not be set manually.
 
    .PARAMETER Ensure
        Specifies whether the npm packages should be present or absent. Defaults to `Present`.
 
    .PARAMETER Global
        Indicates whether to install packages globally.
 
    .PARAMETER PackageDirectory
        The directory containing the `package.json` file. If not specified, the current directory is used.
 
    .PARAMETER Arguments
        Additional arguments to pass to `npm install`, provided as an array of strings where each
        element is a separate argument.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName NpmDsc -Name NpmInstall -Method Set -Property @{
            PackageDirectory = 'C:\repos\my-project'
        }
 
        This example installs all npm packages defined in `C:\repos\my-project\package.json`.
#>

[DSCResource()]
class NpmInstall {
    [DscProperty()]
    [Ensure]$Ensure = [Ensure]::Present

    [DscProperty(Key)]
    [string]$SID

    [DscProperty()]
    [bool]$Global

    [DscProperty()]
    [string]$PackageDirectory

    [DscProperty()]
    [string[]]$Arguments

    [NpmInstall] Get() {
        Assert-Npm

        if (-not([string]::IsNullOrEmpty($this.PackageDirectory))) {
            Set-PackageDirectory -PackageDirectory $this.PackageDirectory
        }

        $currentState = [NpmInstall]::new()
        $currentState.Ensure = [Ensure]::Present

        $errorResult = Get-InstalledNpmPackages -Global $this.Global | ConvertFrom-Json | Select-Object -ExpandProperty error
        if ($errorResult.PSobject.Properties.Name -contains 'code') {
            $errorCode = $errorResult | Select-Object -ExpandProperty code
            if ($errorCode -eq 'ELSPROBLEMS') {
                $currentState.Ensure = [Ensure]::Absent
            }
        }

        $currentState.Global = $this.Global
        $currentstate.PackageDirectory = $this.PackageDirectory
        $currentState.Arguments = $this.Arguments
        return $currentState
    }

    [bool] Test() {
        $currentState = $this.Get()
        return $this.Ensure -eq $currentState.Ensure
    }

    [void] Set() {
        $inDesiredState = $this.Test()
        if ($this.Ensure -eq [Ensure]::Present) {
            if (-not $inDesiredState) {
                Install-NpmPackage -Arguments $this.Arguments -Global $this.Global
            }
        } else {
            if (-not $inDesiredState) {
                $nodeModulesFolder = 'node_modules'
                if (Test-Path -Path $nodeModulesFolder) {
                    Remove-Item $nodeModulesFolder -Recurse
                }
            }
        }
    }
}

<#
.SYNOPSIS
    The `NpmPackage` DSC Resource allows you to manage the installation, update, and removal of npm packages. This resource ensures that the specified npm package is in the desired state.
 
.PARAMETER Ensure
    Specifies whether the npm package should be present or absent. The default value is `Present`.
 
.PARAMETER Name
    The name of the npm package to manage. This is a key property.
 
.PARAMETER Version
    The version of the npm package to install. If not specified, the latest version will be installed.
 
.PARAMETER PackageDirectory
    The directory where the npm package should be installed. If not specified, the package will be installed in the current directory.
 
.PARAMETER Global
    Indicates whether the npm package should be installed globally.
 
.PARAMETER Arguments
    Additional arguments to pass to `npm install` or `npm uninstall`, provided as an array of strings
    where each element is a separate argument.
 
.EXAMPLE
    PS C:\> Invoke-DscResource -ModuleName NpmDsc -Name NpmPackage -Method Set -Property @{ Name = 'react' }
 
    This example installs the npm package 'react' in the current directory.
 
.EXAMPLE
    PS C:\> Invoke-DscResource -ModuleName NpmDsc -Name NpmPackage -Method Set -Property @{ Name = 'babel'; Global = $true }
 
    This example installs the npm package 'babel' globally.
 
.EXAMPLE
    PS C:\> ([NpmPackage]@{ Name = 'react' }).WhatIf()
 
    This example returns the whatif result for installing the npm package 'react'. Note: This does not actually install the package and requires the module to be imported using 'using module <moduleName>'.
#>

[DSCResource()]
class NpmPackage {
    [DscProperty()]
    [Ensure]$Ensure = [Ensure]::Present

    [DscProperty(Key)]
    [string]$Name

    [DscProperty()]
    [string]$Version

    [DscProperty()]
    [string]$PackageDirectory

    [DscProperty()]
    [bool]$Global

    [DscProperty()]
    [string[]]$Arguments

    [NpmPackage] Get() {
        Assert-Npm

        if (-not([string]::IsNullOrEmpty($this.PackageDirectory))) {
            Set-PackageDirectory -PackageDirectory $this.PackageDirectory
        }

        $currentState = [NpmPackage]::new()
        $currentState.Ensure = [Ensure]::Absent

        $installedPackages = Get-InstalledNpmPackages -Global $this.Global | ConvertFrom-Json | Select-Object -ExpandProperty dependencies
        if ($installedPackages.PSobject.Properties.Name -contains $this.Name) {
            $installedPackage = $installedPackages | Select-Object -ExpandProperty $this.Name

            # Check if version matches if specified.
            if (-not([string]::IsNullOrEmpty($this.Version))) {
                $installedVersion = $installedPackage.Version
                if ([System.Version]$installedVersion -eq [System.Version]$this.Version) {
                    $currentState.Ensure = [Ensure]::Present
                }
            } else {
                $currentState.Ensure = [Ensure]::Present
            }
        }

        $currentState.Name = $this.Name
        $currentState.Version = $this.Version
        $currentState.Global = $this.Global
        $currentState.Arguments = $this.Arguments
        $currentState.PackageDirectory = $this.PackageDirectory
        return $currentState
    }

    [bool] Test() {
        $currentState = $this.Get()
        return $this.Ensure -eq $currentState.Ensure
    }

    [void] Set() {
        $inDesiredState = $this.Test()
        if ($this.Ensure -eq [Ensure]::Present) {
            if (-not $inDesiredState) {
                Install-NpmPackage -PackageName $this.Name -Arguments $this.Arguments -Global $this.Global
            }
        } else {
            if (-not $inDesiredState) {
                Uninstall-NpmPackage -PackageName $this.Name -Arguments $this.Arguments -Global $this.Global
            }
        }
    }

    static [NpmPackage[]] Export() {
        $packages = Get-InstalledNpmPackages -Global $true | ConvertFrom-Json -AsHashtable | Select-Object -ExpandProperty dependencies
        $out = [List[NpmPackage]]::new()
        $globalDir = (Join-Path -Path (Invoke-Npm -Command @('config', 'get', 'prefix')) -ChildPath 'node_modules')
        foreach ($package in $packages.GetEnumerator()) {
            $in = [NpmPackage]@{
                Name             = $package.Name
                Version          = $package.Value.version
                Ensure           = [Ensure]::Present
                Global           = $true
                Arguments        = $null
                PackageDirectory = $globalDir
            }

            $out.Add($in)
        }

        return $out
    }

    [string] WhatIf() {
        if ($this.Ensure -eq [Ensure]::Present) {
            $out = @{
                Name      = $this.Name
                _metaData = @{
                    whatif = @()
                }
            }

            try {
                $whatIfState = Install-NpmPackage -PackageName $this.Name -Global $this.Global -Arguments @('--dry-run')
                $out._metaData.whatif = $whatIfState | Where-Object { $_.Trim() -ne '' } # Removes empty lines from response
            } catch {
                $out._metaData.whatif = GetNpmWhatIfResponse
            }
        } else {
            # Uninstall does not have --dry-run param
            $out = @{}
        }

        return ($out | ConvertTo-Json -Depth 10 -Compress)
    }
}
#endRegion DSCResources

# SIG # Begin signature block
# MIInRgYJKoZIhvcNAQcCoIInNzCCJzMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAltUNqqpYoFh/E
# XCJR1kTfuo9sCi4R9RjmIfQQlIYFt6CCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1
# OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8
# o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg
# 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4
# Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R
# X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk
# ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw
# HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg
# UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh
# MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9
# s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H
# VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3
# w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n
# 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs
# A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo
# Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb
# SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6
# 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z
# V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v
# 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs
# /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA
# AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow
# VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh
# emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h
# KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd
# M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp
# yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t
# Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5
# REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs
# 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK
# Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5
# pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW
# eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ
# 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC
# NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI
# MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4
# NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh
# ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q
# hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU
# nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb
# H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z
# uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u
# vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW
# 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV
# DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghniMIIZ3gIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK
# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEII64uLF5
# /AkigOGgGb6FbzPS2goINYoJvoI4Cydo05XYMEIGCisGAQQBgjcCAQwxNDAyoBSA
# EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w
# DQYJKoZIhvcNAQEBBQAEggEAoII75T5L34Bc7GIhQ5yr9UI8SoEtCr+qFf1axDE1
# WjQ05vg48PA1eE8FyPHDZYlFslfRfbylac1tFluRUhnKgYVmcCbvj29d7riBUiQb
# 8GULb9N576udq13aRJKSC4+I7ytW70UwuwHr97EiRZROH2s6r3O0JixXG8g6c/IK
# C2fl/EhcepvhseEeKiZI89NNAgHfuc9frxP1HPDnzEGqf4zRednzbTyfTGw+9vxc
# e5HCF6qX2+jSgoIP4W++ROoW/zC6Ek41dfK1NyyAaYYwOHGlJyeWSmTLpyXks7/Q
# ON45eu2F2Eb2T79uP0bx4x3HFaAkhW3M7HjGcsFaTRsjMKGCF5QwgheQBgorBgEE
# AYI3AwMBMYIXgDCCF3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUD
# BAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoD
# ATAxMA0GCWCGSAFlAwQCAQUABCByBkUEVEoZjRMpl2K/oOyId+6N3dHYMf0mVHXH
# dmSgjAIGahdP1/AsGBMyMDI2MDYwOTIzNDUwMC4zNjdaMASAAgH0oIHRpIHOMIHL
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN
# aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRT
# UyBFU046ODYwMy0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFNlcnZpY2WgghHqMIIHIDCCBQigAwIBAgITMwAAAiWAxzfGzap3SQABAAAC
# JTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAe
# Fw0yNjAyMTkxOTQwMDFaFw0yNzA1MTcxOTQwMDFaMIHLMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj
# YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0wNUUw
# LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCm8RIP0eLA46VcCPovvmqsIlN6
# qkmz5IsHWmUU0neUqp8uGxadeo+SwWBCwQ5alZI/DNdpXfyiZLZR6XYgpRPFzepI
# l7OCDb4NtEskJCIZDkQMNwrH9YwUyu71GGigsLIxeleHtA3utoVTeHjS1b8UnwOR
# RtknKkyrUArT6ZpB2rodIcmcLcv3x3wwgYlOs0FEg5EsVrZb7LNc/nd0bXDp+HTO
# WWui8eoTVwJeLxcVP869oF8li5SU81aa2tGJ6/Jsejiz9JMW8SJXKBT2DCXMOUkC
# sGjonPZRqfvoMSIQZgtaOTyAJlrvsy0TZ78XrGqoygtQimQnbOAL4KNLSCuW5TZE
# QGTHLOQJGgggb3j5gKC778+RIPJA+n/hmHJ/x4qT/HTTPoVeMCcuBKWrQXR1+/pY
# au3Fwe0tWIyG+LWzkRr/ZNPPupcA2Yci3qn8HR9RwvQopqSNJwn2Ri6am8AQyfVV
# y/BBw0t6jpoRPjwKvuUjfCzpae6duOxQtQ1XDN9PA2yl9sDko/+AXV/SOe8ea8Qo
# Qcv3s3ErkG+Lp6hnvw6OMPian4ggNkRtgtB7ro1OiopOUXJn9Y5EO3JUAXNcuM9m
# +5My1VEuvGytgAH3uxmslTnW3YbrfazaySCSSnWkhaOZ33hgbuUQfH7n2NFEAUc/
# cFzfmCQUikWisnJYywIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFLE40qoXTuMHX3Af
# ZUu1n8nx2h93MB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1Ud
# HwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggr
# BgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw
# MTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgw
# DgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQAHnfc2yUyoHZbvvyVK
# FuXh5HxxHIvIaR9JWpIfITJlc/Ki03juR+vckzq3tp5fFH5LL7eIFXRIuoewMsvW
# eFrWufrrW4HhmhCwkqArfA1C0xk+HaYs2O48YSxMX9lgS1kTTIb3YsfoFdFpKurP
# f2nc2Yd4wLg+FgwmkxkeyE3MUKVna8SZeVpEjnS5ucFck4srPwK2ORAf70I23GGy
# PhqgIKZphNXhSscTAQsyIqB5GwDMdRV5LK37NfU4YmxvCYh3TFYE/Gh01Q6yJvf9
# HxiEZpwW+oUk0gruHobg3sgIR5rfgUo8l30vUnaDYMcPAClaFMC/QbHZSaUhWXZG
# 1OOcMp0g9vYQNLDEqFX2jlquvzVSSwtHtm1KTldCjRED+kdCybcPxbPalwJigXc1
# BsI9CitnTf0ljwb9NkZ/JVI8/D62rXXzhz4F3u0iVGzwncGaxRxHG/Xv4nTrpkOe
# epoYbNBbMWS2G1qP3Xj7pVf0+4qRyAqJ0stjQjoVOJImVPWRjz5PR3Dn6adQVMBJ
# DM6gDrj1rZTFVgCtTijqGZSGzvXpGkF3vYsyE6ZDma/kGdiUe5saeI6lH66PiWWX
# gqxt7sy2Ezv0yIjSVv+eMOT2QMUiZ6WCc7gVtAmXpfeIus+NmgFvM+Ic1X58e4I9
# EL4ZSAidSpWW0GZTLNC02mryLjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkA
# AAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVow
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX
# 9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1q
# UoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8d
# q6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byN
# pOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2k
# rnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4d
# Pf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgS
# Uei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8
# QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6Cm
# gyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzF
# ER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQID
# AQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQU
# KqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1
# GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0
# bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA
# QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL
# j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p
# Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwU
# tj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN
# 3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU
# 5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5
# KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGy
# qVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB6
# 2FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltE
# AY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFp
# AUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcd
# FYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRb
# atGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQd
# VTNYs6FwZvKhggNNMIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjg2MDMtMDVFMC1E
# OTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEw
# BwYFKw4DAhoDFQBTb+bKOPAjCBflhzw5EXBuSWxeDqCBgzCBgKR+MHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7dLwCTAiGA8y
# MDI2MDYwOTIwMDMyMVoYDzIwMjYwNjEwMjAwMzIxWjB0MDoGCisGAQQBhFkKBAEx
# LDAqMAoCBQDt0vAJAgEAMAcCAQACAgIjMAcCAQACAhNQMAoCBQDt1EGJAgEAMDYG
# CisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEA
# AgMBhqAwDQYJKoZIhvcNAQELBQADggEBACUppvG5zq6eJCGzyKNJAhzktOdr8CWe
# H4ZRDtcb9UZBBfHdAa/OolGPNGpjT7/CyCCagFhp52PSOJKhv7IXzPIBytgAD1yC
# JhnmNdVyRzEVDxfUdYBfx20earDZRBI75z23dDnDzGworrMy/CtCUwcw9MFxxjlM
# xPLyxQgqferDqEvD1unxh9CW79kGj7sPtpqxcSLYmKCVR1EP/WyqMRf5QIMVmnUo
# HxmX8knpJRTsjHydB/7/NYedNpEJFDU9rRqDu7Bpli0T/gSyW2y22I5Da22N8y/H
# IA1HA8f0H5NAmSzohBb2ncTlOUAXp0iO8nwk0jWU0Y3PbpqLNh49VOcxggQNMIIE
# CQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYw
# JAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAiWAxzfG
# zap3SQABAAACJTANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqG
# SIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCCxoim3bibnRf+kpSN+4aLBWlvzCER0
# 3seksTyY8h0kFTCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIFYN7oh6ON3y
# 92CmAl/lF0CYwrjWWQP6dCUxajPSHKEQMIGYMIGApH4wfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTACEzMAAAIlgMc3xs2qd0kAAQAAAiUwIgQgNkm+27fJUWw1
# bxp5f3uo5v5jPpLSNNBl3RtVVW6G8JIwDQYJKoZIhvcNAQELBQAEggIAoJzGllme
# G+yOsUfeYtTfgbI2JwYJ39qECMJK+Ff9GmozJx7JOj/Jr3qdjWGxe2wtQgPZGgji
# D1ubYy/KXaqMvML28RQ7qQSnL/fiCH3yUzXMkkSv33G15aMiTLM3Bw/GDiA1BCv0
# nNAQnJ1mgDrNT8Yj6Si7QTI+r1Nm+iGTOoGhbmvl+6Oizc/hNJfu0nvDFC3tMWt2
# cQNO9NXNr0rxeEcy1Z3zAFRUCtux9kX7ZuiGmeGCgyL3IKh2TH3D59pTHw1ehDUq
# pn9pSFy85/tjKoKpYW7znuDgzM56f5Y/v8toeUtAQ08XJqPv3FTOj6JopId7/H7l
# LQ+5nStl3/cHy2OxDtIViqCJStcPTK/KbDyamGReNjbFk/thT06JtEqpjYgoTYSE
# LiFP1nvS5p8RaL3uh6i/GYP0eN+v2OGHfwdJKqTYdjaizzq15GAox65UPYWMvKAO
# ugGMNd0YzpRCRXFBYmw44pxdOcrdbAnLwNhkSasUzOQcT9gXUMS8JYGNiYWPqQ6u
# ZVeaVVXMGiLfTqTm061kIFrZw/ySQKza0Z4mVOVkSqxb3N0Ukf6qpboXlLr2DYtE
# 4P+/5Gvxcm5l3iCgoXpDz8u3a0olqh+leATySYUFQok/YbzxBRmGOffP1k859HOw
# mU+vAAs5hQdvDM/RrcEfe8e0ood3rCWAsCE=
# SIG # End signature block