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 |