Microsoft.AVS.CDR.psm1
|
$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest class DependencyGraphNode { [string]$Name [string]$Version [System.Collections.ArrayList]$Dependencies [bool]$NotFound [string]$Repository [string]$InstalledLocation DependencyGraphNode( [string]$Name, [string]$Version, [System.Collections.IList]$Dependencies, [bool]$NotFound, [string]$Repository, [string]$InstalledLocation ) { $this.Name = $Name $this.Version = $Version $this.Dependencies = [System.Collections.ArrayList]::new($Dependencies) $this.NotFound = $NotFound $this.Repository = $Repository $this.InstalledLocation = $InstalledLocation } } $script:defaultRedirectMap = @{ } $script:moduleMapCache = @{ } <# .SYNOPSIS Finds and validates a redirect for a dependency version. .PARAMETER RedirectMap Map entries: "Name@Version" -> "NewVersion", "Name" -> "Version", "Name@Version" -> "*" or "Name" -> "*" (retain version, normalize casing). .OUTPUTS Hashtable with ResolvedVersion, ResolvedName, and IsRedirected. #> function Find-DependencyRedirect { param( [Parameter(Mandatory = $true)] [string]$DependencyName, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string]$DependencyVersion, [Parameter(Mandatory = $true)] [hashtable]$RedirectMap, [Parameter(Mandatory = $false)] [string]$Indent = "" ) if ([string]::IsNullOrWhiteSpace($DependencyVersion)) { if ($RedirectMap.ContainsKey($DependencyName)) { $depVersion = $RedirectMap[$DependencyName] Write-Verbose "${Indent}Resolved unversioned dependency: $DependencyName -> $depVersion (from redirect map)" $resolvedName = $DependencyName foreach ($entry in $RedirectMap.GetEnumerator()) { if ($entry.Key -eq $DependencyName) { $resolvedName = $entry.Key break } } return @{ ResolvedVersion = $depVersion ResolvedName = $resolvedName IsRedirected = $true } } else { throw "${Indent}Cannot conservatively resolve version for dependency '$DependencyName'. Please add a redirect mapping for this module." } } $isExactVersion = $true $normalizedDepVersion = $DependencyVersion # Version range like "[1.0, 1.0]", "[1.0, )", "(, 2.0]" if ($DependencyVersion -match '^(\[|\()([^,]*),\s*([^\]\)]*)(\]|\))$') { $openBracket = $matches[1] $minVer = $matches[2] $maxVer = $matches[3] $closeBracket = $matches[4] if ($minVer -and $maxVer -and ($minVer -eq $maxVer) -and ($openBracket -eq '[') -and ($closeBracket -eq ']')) { $normalizedDepVersion = $minVer # exact: [1.0, 1.0] } elseif ($maxVer -and (-not $minVer)) { $isExactVersion = $false $normalizedDepVersion = $maxVer # open-ended: (, 2.0] } elseif ($minVer -and (-not $maxVer)) { $isExactVersion = $false $normalizedDepVersion = $minVer # open-ended: [1.0, ) } else { $isExactVersion = $false $normalizedDepVersion = $minVer } } # Check name@version first, then name-only fallback $depKeyPattern = "${DependencyName}@${normalizedDepVersion}" $versionSpecificEntry = $null $nameOnlyEntry = $null foreach ($entry in $RedirectMap.GetEnumerator()) { if ($entry.Key -eq $depKeyPattern) { $versionSpecificEntry = $entry break } elseif ($null -eq $nameOnlyEntry -and $entry.Key -eq $DependencyName) { $nameOnlyEntry = $entry } } $matchedEntry = if ($versionSpecificEntry) { $versionSpecificEntry } else { $nameOnlyEntry } $isNameOnlyMatch = $null -eq $versionSpecificEntry -and $null -ne $nameOnlyEntry if ($matchedEntry) { $resolvedVersion = $matchedEntry.Value # "*" retains version but normalizes dependency name casing if ($resolvedVersion -eq "*") { $resolvedVersion = $normalizedDepVersion if ($isNameOnlyMatch) { $resolvedName = $matchedEntry.Key } else { $resolvedName = $matchedEntry.Key -replace '@.*$', '' } Write-Verbose "${Indent}Normalizing dependency name: $DependencyName -> $resolvedName (version $normalizedDepVersion retained)" return @{ ResolvedVersion = $resolvedVersion ResolvedName = $resolvedName IsRedirected = $true } } if ($isExactVersion -and $resolvedVersion -ne $normalizedDepVersion) { throw "${Indent}Cannot redirect exact version dependency '$DependencyName' from version $normalizedDepVersion to $resolvedVersion. Exact version specifications must redirect to the same version or have no redirect." } if ($isNameOnlyMatch) { $resolvedName = $matchedEntry.Key Write-Verbose "${Indent}Redirecting dependency: $DependencyName $DependencyVersion -> $resolvedVersion (from name-only redirect)" } else { $resolvedName = $matchedEntry.Key -replace '@.*$', '' Write-Verbose "${Indent}Redirecting dependency: $DependencyName $DependencyVersion -> $resolvedVersion (from redirect map)" } return @{ ResolvedVersion = $resolvedVersion ResolvedName = $resolvedName IsRedirected = $true } } else { return @{ ResolvedVersion = $normalizedDepVersion ResolvedName = $DependencyName IsRedirected = $false } } } <# .SYNOPSIS Merges redirect maps — OuterMap takes precedence. Loads module-specific map files from maps/ dir. #> function Get-MergedRedirectMap { param( [Parameter(Mandatory = $true)] [hashtable]$OuterMap, [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Version ) $redirectMap = $OuterMap if ([string]::IsNullOrWhiteSpace($Version)) { $versionPatterns = @() } else { $baseVersion = $Version if ($Version -match '[\[\(]([0-9][0-9a-zA-Z.\-]*)') { $baseVersion = $matches[1] } $versionParts = $baseVersion -split '[.\-]' $major = $versionParts[0] $minor = if ($versionParts.Count -gt 1) { $versionParts[1] } else { "0" } $versionPatterns = @( $baseVersion, # Full version (e.g., 1.4.0.15939652 or 1.0.0-preview) "$major.$minor", # Major.Minor (e.g., 1.4) "$major" # Major only (e.g., 1) ) } $moduleMap = $null foreach ($versionPattern in $versionPatterns) { $cacheKey = "$Name@$versionPattern" if ($script:moduleMapCache.ContainsKey($cacheKey)) { Write-Verbose "Using cached redirect map for: $cacheKey" $moduleMap = $script:moduleMapCache[$cacheKey] break } $testPath = Join-Path $PSScriptRoot "maps" "$Name@$versionPattern.json" if (Test-Path $testPath) { Write-Verbose "Loading module-specific redirect map from: $testPath" $moduleMap = Get-Content $testPath -Raw | ConvertFrom-Json -AsHashtable $script:moduleMapCache[$cacheKey] = $moduleMap break } } if ($moduleMap) { $mergedMap = @{} foreach ($key in $moduleMap.Keys) { $mergedMap[$key] = $moduleMap[$key] } foreach ($key in $redirectMap.Keys) { $mergedMap[$key] = $redirectMap[$key] # Outer map wins } $redirectMap = $mergedMap } return $redirectMap } <# .SYNOPSIS Builds a dependency graph by recursively querying a remote repository via Find-PSResource. #> function Build-RemoteDependencyGraph { param( [Parameter(Mandatory = $true)] [string]$ModuleName, [Parameter(Mandatory = $true)] [string]$ModuleVersion, [Parameter(Mandatory = $true)] [hashtable]$Graph, [Parameter(Mandatory = $true)] [hashtable]$RedirectMap, [Parameter(Mandatory = $false)] [string]$Repository, [Parameter(Mandatory = $false)] [PSCredential]$Credential, [Parameter(Mandatory = $false)] [switch]$Prerelease, [Parameter(Mandatory = $false)] [int]$Depth = 0 ) $indent = " " * $Depth $moduleKey = "${ModuleName}@${ModuleVersion}" # Skip if already processed if ($Graph.ContainsKey($moduleKey)) { Write-Verbose "${indent}Already in graph: $moduleKey" return } $findParams = @{ Name = $ModuleName Version = $ModuleVersion } if ($Repository) { $findParams['Repository'] = $Repository } if ($Credential) { $findParams['Credential'] = $Credential } if ($Prerelease) { $findParams['Prerelease'] = $Prerelease } Write-Verbose "Looking for dependencies: $ModuleName version $ModuleVersion" $moduleInfo = Find-PSResource @findParams -ErrorAction SilentlyContinue | Select-Object -First 1 $notFound = $false if (-not $moduleInfo) { Write-Verbose "${indent}Module not found in repository: $ModuleName version $ModuleVersion (will validate after resolution)" $notFound = $true } Write-Verbose "${indent}Building graph for: $ModuleName version $ModuleVersion" $graphNode = [DependencyGraphNode]::new( $ModuleName, $ModuleVersion, [System.Collections.ArrayList]@(), $notFound, $(if ($moduleInfo) { $moduleInfo.Repository } else { $null }), $null ) $Graph[$moduleKey] = $graphNode if ($notFound) { return } $deps = $moduleInfo.Dependencies if (-not $deps -or $deps.Count -eq 0) { Write-Verbose "${indent}No dependencies for $ModuleName" return } Write-Verbose "${indent}Found $($deps.Count) dependency(ies)" foreach ($dep in $deps) { $depName = $dep.Name $depVersion = $dep.VersionRange $redirectResult = Find-DependencyRedirect -DependencyName $depName -DependencyVersion $depVersion ` -RedirectMap $RedirectMap -Indent $indent $resolvedDepVersion = $redirectResult.ResolvedVersion $resolvedDepName = $redirectResult.ResolvedName $depKey = "${resolvedDepName}@${resolvedDepVersion}" Write-Verbose "${indent} Dependency: $depKey" [void]$graphNode.Dependencies.Add($depKey) $depRedirectMap = Get-MergedRedirectMap -OuterMap $RedirectMap -Name $resolvedDepName -Version $resolvedDepVersion Build-RemoteDependencyGraph -ModuleName $resolvedDepName -ModuleVersion $resolvedDepVersion ` -Graph $Graph -RedirectMap $depRedirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease -Depth ($Depth + 1) } } <# .SYNOPSIS Compares two semver strings including prerelease labels. Returns -1, 0, or 1. Prerelease < release (1.0.0-alpha < 1.0.0). #> function Compare-SemVer { param( [Parameter(Mandatory = $true)] [string]$Version1, [Parameter(Mandatory = $true)] [string]$Version2 ) $v1Parts = $Version1 -split '-', 2 $v2Parts = $Version2 -split '-', 2 $v1Base = $v1Parts[0] $v2Base = $v2Parts[0] $v1Prerelease = if ($v1Parts.Count -gt 1) { $v1Parts[1] } else { $null } $v2Prerelease = if ($v2Parts.Count -gt 1) { $v2Parts[1] } else { $null } try { $v1Ver = [System.Version]$v1Base $v2Ver = [System.Version]$v2Base $baseCompare = $v1Ver.CompareTo($v2Ver) } catch { $baseCompare = [string]::Compare($v1Base, $v2Base, [StringComparison]::OrdinalIgnoreCase) } if ($baseCompare -ne 0) { return $baseCompare } # No prerelease > any prerelease (1.0.0 > 1.0.0-alpha) if ($null -eq $v1Prerelease -and $null -eq $v2Prerelease) { return 0 } if ($null -eq $v1Prerelease) { return 1 # v1 is release, v2 is prerelease } if ($null -eq $v2Prerelease) { return -1 # v1 is prerelease, v2 is release } # Both have prerelease — lexicographic: alpha < beta < dev < rc return [string]::Compare($v1Prerelease, $v2Prerelease, [StringComparison]::OrdinalIgnoreCase) } <# .SYNOPSIS Resolves diamond dependencies — selects the highest found version and updates all graph references. #> function Resolve-DiamondDependencies { param( [Parameter(Mandatory = $true)] [hashtable]$Graph ) # Group nodes by module name $moduleVersions = @{} foreach ($nodeKey in $Graph.Keys) { $node = $Graph[$nodeKey] $moduleName = $node.Name if (-not $moduleVersions.ContainsKey($moduleName)) { $moduleVersions[$moduleName] = @() } $moduleVersions[$moduleName] += @{ Key = $nodeKey VersionString = $node.Version Node = $node NotFound = $node.NotFound } } # Resolve conflicts — prefer found versions, then highest semver foreach ($moduleName in $moduleVersions.Keys) { $versions = $moduleVersions[$moduleName] if ($versions.Count -gt 1) { $sorted = $versions | Sort-Object -Property @{ Expression = { if ($_.NotFound) { "1" } else { "0" } } }, @{ Expression = { $ver = $_.VersionString $parts = $ver -split '-', 2 $base = $parts[0] $prerelease = if ($parts.Count -gt 1) { $parts[1] } else { $null } $verParts = $base -split '\.' $paddedBase = ($verParts | ForEach-Object { $_.PadLeft(10, '0') }) -join '.' # Release versions sort after prereleases $prereleaseKey = if ($null -eq $prerelease) { 'zzzzzzzzzz' } else { $prerelease } "$paddedBase|$prereleaseKey" } Descending = $true } $highest = $sorted[0] $conflicts = $sorted | Select-Object -Skip 1 $discardedNotFound = $conflicts | Where-Object { $_.NotFound } if ($discardedNotFound) { Write-Verbose " Discarding unavailable version(s): $($discardedNotFound.VersionString -join ', ') (higher version available)" } Write-Warning "Diamond dependency detected for '$moduleName': versions $($versions.VersionString -join ', '). Using highest available: $($highest.VersionString)" foreach ($conflict in $conflicts) { $oldKey = $conflict.Key $newKey = $highest.Key Write-Verbose " Redirecting $oldKey -> $newKey" # Update all references from old version to new foreach ($nodeKey in $Graph.Keys) { $node = $Graph[$nodeKey] for ($i = 0; $i -lt $node.Dependencies.Count; $i++) { if ($node.Dependencies[$i] -eq $oldKey) { $node.Dependencies[$i] = $newKey } } } $Graph.Remove($oldKey) } } } # Validate all remaining nodes were found foreach ($nodeKey in $Graph.Keys) { $node = $Graph[$nodeKey] if ($node.NotFound) { throw "Module not found: $($node.Name) version $($node.Version). No alternative version available to satisfy the dependency." } } } <# .SYNOPSIS Returns module keys in topological order (dependencies first). Warns on cycles. #> function Get-TopologicalOrder { param( [Parameter(Mandatory = $true)] [hashtable]$Graph ) $visited = @{} $visiting = @{} # For cycle detection $order = [System.Collections.ArrayList]@() function Visit { param([string]$NodeKey) if ($visited.ContainsKey($NodeKey)) { return } if ($visiting.ContainsKey($NodeKey)) { Write-Warning "Circular dependency detected involving: $NodeKey" return } $visiting[$NodeKey] = $true if ($Graph.ContainsKey($NodeKey)) { $node = $Graph[$NodeKey] foreach ($depKey in $node.Dependencies) { Visit -NodeKey $depKey } } $visiting.Remove($NodeKey) $visited[$NodeKey] = $true [void]$order.Add($NodeKey) } # Visit all nodes foreach ($nodeKey in $Graph.Keys) { Visit -NodeKey $nodeKey } return $order.ToArray() } <# .SYNOPSIS Builds a dependency graph for installed modules via Get-PSResource. #> function Build-InstalledDependencyGraph { param( [Parameter(Mandatory = $true)] [string]$ModuleName, [Parameter(Mandatory = $true)] [string]$ModuleVersion, [Parameter(Mandatory = $true)] [hashtable]$Graph, [Parameter(Mandatory = $true)] [hashtable]$RedirectMap, [Parameter(Mandatory = $false)] [int]$Depth = 0 ) $indent = " " * $Depth $moduleKey = "${ModuleName}@${ModuleVersion}" # Skip if already processed if ($Graph.ContainsKey($moduleKey)) { Write-Verbose "${indent}Already in graph: $moduleKey" return } # Find the installed module $installedModule = Get-PSResource -Name $ModuleName -Version $ModuleVersion -ErrorAction SilentlyContinue | Select-Object -First 1 $notFound = $false if (-not $installedModule) { Write-Verbose "${indent}Module not installed: $ModuleName version $ModuleVersion (will validate after resolution)" $notFound = $true } $actualVersion = if ($installedModule) { $installedModule.Version.ToString() } else { $ModuleVersion } Write-Verbose "${indent}Building graph for: $ModuleName version $actualVersion" # InstalledLocation is the base modules folder; append ModuleName/Version $moduleVersionPath = if ($installedModule) { Join-Path $installedModule.InstalledLocation $ModuleName $actualVersion } else { $null } $graphNode = [DependencyGraphNode]::new( $ModuleName, $actualVersion, [System.Collections.ArrayList]@(), $notFound, $null, $moduleVersionPath ) $Graph[$moduleKey] = $graphNode if ($notFound) { return } $deps = $installedModule.Dependencies if (-not $deps -or $deps.Count -eq 0) { Write-Verbose "${indent}No dependencies for $ModuleName" return } Write-Verbose "${indent}Found $($deps.Count) dependency(ies)" foreach ($dep in $deps) { $depName = $dep.Name $depVersion = $dep.VersionRange $redirectResult = Find-DependencyRedirect -DependencyName $depName -DependencyVersion $depVersion ` -RedirectMap $RedirectMap -Indent $indent $resolvedDepVersion = $redirectResult.ResolvedVersion $resolvedDepName = $redirectResult.ResolvedName $depKey = "${resolvedDepName}@${resolvedDepVersion}" Write-Verbose "${indent} Dependency: $depKey" [void]$graphNode.Dependencies.Add($depKey) $depRedirectMap = Get-MergedRedirectMap -OuterMap $RedirectMap -Name $resolvedDepName -Version $resolvedDepVersion Build-InstalledDependencyGraph -ModuleName $resolvedDepName -ModuleVersion $resolvedDepVersion ` -Graph $Graph -RedirectMap $depRedirectMap -Depth ($Depth + 1) } } function Install-PSResourcePinned { <# .SYNOPSIS Installs a module with pinned dependency versions. Works around PowerCLI not following semver (13.4 breaks backward-compat). .EXAMPLE Install-PSResourcePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$RequiredVersion, [Parameter(Mandatory = $false)] [string]$RedirectMapPath, [Parameter(Mandatory = $false)] [ValidateSet('CurrentUser', 'AllUsers')] [string]$Scope = 'CurrentUser', [Parameter(Mandatory = $false)] [string]$Repository, [Parameter(Mandatory = $false)] [PSCredential]$Credential, [Parameter(Mandatory = $false)] [switch]$Prerelease, [Parameter(Mandatory = $false)] [switch]$Force ) # Load redirect map if ($RedirectMapPath) { if (-not (Test-Path $RedirectMapPath)) { throw "Redirect map file not found: $RedirectMapPath" } Write-Verbose "Loading redirect map from: $RedirectMapPath" $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable } else { Write-Verbose "Using default redirect map" $redirectMap = $script:defaultRedirectMap } $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion Write-Verbose "Building dependency graph for $Name version $RequiredVersion" $dependencyGraph = @{} Build-RemoteDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion ` -Graph $dependencyGraph -RedirectMap $redirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease Resolve-DiamondDependencies -Graph $dependencyGraph Write-Verbose "Computing topological order" $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph) Write-Verbose "Install order ($($topologicalOrder.Count) modules):" for ($i = 0; $i -lt $topologicalOrder.Count; $i++) { Write-Verbose " $($i + 1). $($topologicalOrder[$i])" } # Install modules in topological order foreach ($moduleKey in $topologicalOrder) { $node = $dependencyGraph[$moduleKey] $modName = $node.Name $modVersion = $node.Version $installed = $null if (-not $Force) { $installed = Get-PSResource -Name $modName -ErrorAction SilentlyContinue | Where-Object { if (-not $_) { return $false } $installedVersion = $_.Version.ToString() if ($_.Prerelease) { $installedVersion = "$installedVersion-$($_.Prerelease)" } $installedVersion -eq $modVersion } } if (-not $installed) { Write-Verbose "Installing: $modName version $modVersion" $installParams = @{ Name = $modName Version = $modVersion Scope = $Scope Prerelease = $Prerelease TrustRepository = $true SkipDependencyCheck = $true } if ($Repository) { $installParams['Repository'] = $Repository } if ($Credential) { $installParams['Credential'] = $Credential } if ($Force) { $installParams['Reinstall'] = $true } Install-PSResource @installParams } else { Write-Verbose "Already installed: $modName version $modVersion" } } $mainModuleKey = "${Name}@${RequiredVersion}" $mainNode = $dependencyGraph[$mainModuleKey] Write-Host "Successfully installed $Name version $($mainNode.Version)" } function Save-PSResourcePinned { <# .SYNOPSIS Downloads a module and its dependencies with pinned versions. Saves as expanded module folders by default; pass -AsNupkg to save as NuGet packages. .EXAMPLE Save-PSResourcePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0" -Path "./packages" .EXAMPLE Save-PSResourcePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0" -Path "./packages" -AsNupkg #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$RequiredVersion, [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $false)] [string]$RedirectMapPath, [Parameter(Mandatory = $false)] [string]$Repository, [Parameter(Mandatory = $false)] [PSCredential]$Credential, [Parameter(Mandatory = $false)] [switch]$AsNupkg, [Parameter(Mandatory = $false)] [switch]$Prerelease ) # Validate and create destination path if (-not (Test-Path $Path)) { Write-Verbose "Creating destination directory: $Path" New-Item -ItemType Directory -Path $Path -Force | Out-Null } $resolvedPath = Resolve-Path $Path Write-Verbose "Saving packages to: $resolvedPath" # Load redirect map if ($RedirectMapPath) { if (-not (Test-Path $RedirectMapPath)) { throw "Redirect map file not found: $RedirectMapPath" } Write-Verbose "Loading redirect map from: $RedirectMapPath" $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable } else { Write-Verbose "Using default redirect map" $redirectMap = $script:defaultRedirectMap } $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion Write-Verbose "Building dependency graph for $Name version $RequiredVersion" $dependencyGraph = @{} Build-RemoteDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion ` -Graph $dependencyGraph -RedirectMap $redirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease Resolve-DiamondDependencies -Graph $dependencyGraph Write-Verbose "Computing topological order" $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph) Write-Verbose "Save order ($($topologicalOrder.Count) modules):" for ($i = 0; $i -lt $topologicalOrder.Count; $i++) { Write-Verbose " $($i + 1). $($topologicalOrder[$i])" } # Save modules in topological order foreach ($moduleKey in $topologicalOrder) { $node = $dependencyGraph[$moduleKey] $modName = $node.Name $modVersion = $node.Version # Existing-output detection depends on output format: # -AsNupkg -> $Path/$modName.$modVersion.nupkg # (default) -> $Path/$modName/$modVersion (expanded module folder) if ($AsNupkg) { $expectedPath = Join-Path $resolvedPath.Path "$modName.$modVersion.nupkg" } else { $expectedPath = Join-Path $resolvedPath.Path $modName $modVersion } if (-not (Test-Path $expectedPath)) { Write-Verbose "Saving: $modName version $modVersion" $saveParams = @{ Name = $modName Version = $modVersion Path = $resolvedPath.Path Prerelease = $Prerelease TrustRepository = $true SkipDependencyCheck = $true } if ($AsNupkg) { $saveParams['AsNupkg'] = $true } if ($Repository) { $saveParams['Repository'] = $Repository } if ($Credential) { $saveParams['Credential'] = $Credential } Save-PSResource @saveParams } else { Write-Verbose "Already saved: $modName version $modVersion" } } $mainModuleKey = "${Name}@${RequiredVersion}" $mainNode = $dependencyGraph[$mainModuleKey] Write-Host "Successfully saved $Name version $($mainNode.Version) and dependencies to $resolvedPath" } <# .SYNOPSIS Extracts RequiredModules and ModuleList from a manifest, deduped (RequiredModules wins). NuGet feeds package both as dependencies; reading both keeps the local graph consistent with remote graphs and prevents "assembly already loaded" errors during pre-loading. .OUTPUTS Array of @{ Name; Version } hashtables. #> function Get-ManifestModuleDependencies { param( [Parameter(Mandatory = $true)] [hashtable]$Manifest ) $hasRequired = $Manifest.ContainsKey('RequiredModules') -and $Manifest.RequiredModules -and $Manifest.RequiredModules.Count -gt 0 $hasModuleList = $Manifest.ContainsKey('ModuleList') -and $Manifest.ModuleList -and $Manifest.ModuleList.Count -gt 0 if (-not $hasRequired -and -not $hasModuleList) { return @() } # Parse a single manifest entry into @{ Name; Version } function ParseEntry { param([object]$Entry, [string]$Source) if ($Entry -is [string]) { throw "$Source entry '$Entry' has no version. All entries must specify a version (RequiredVersion or ModuleVersion)." } elseif ($Entry -is [hashtable]) { $name = if ($Entry.ContainsKey('ModuleName')) { $Entry.ModuleName } else { $null } if (-not $name) { throw "$Source entry has no module name: $($Entry | ConvertTo-Json -Compress)" } $version = $null if ($Entry.ContainsKey('RequiredVersion')) { $version = $Entry.RequiredVersion.ToString() } elseif ($Entry.ContainsKey('ModuleVersion')) { $version = "[$($Entry.ModuleVersion), )" } if (-not $version) { throw "$Source entry '$name' has no version. All entries must specify a version (RequiredVersion or ModuleVersion)." } return @{ Name = $name; Version = $version } } else { throw "Unrecognized $Source format in manifest: $Entry. Expected string or hashtable." } } # RequiredModules take precedence $seen = @{} $results = [System.Collections.ArrayList]@() if ($hasRequired) { Write-Verbose "Found $($Manifest.RequiredModules.Count) module(s) in RequiredModules" foreach ($entry in $Manifest.RequiredModules) { $parsed = ParseEntry -Entry $entry -Source 'RequiredModules' $key = $parsed.Name.ToLowerInvariant() if (-not $seen.ContainsKey($key)) { $seen[$key] = $true [void]$results.Add($parsed) } } } if ($hasModuleList) { Write-Verbose "Found $($Manifest.ModuleList.Count) module(s) in ModuleList" foreach ($entry in $Manifest.ModuleList) { $parsed = ParseEntry -Entry $entry -Source 'ModuleList' $key = $parsed.Name.ToLowerInvariant() if (-not $seen.ContainsKey($key)) { $seen[$key] = $true [void]$results.Add($parsed) } else { Write-Verbose "Skipping ModuleList entry '$($parsed.Name)' — already declared in RequiredModules" } } } return $results.ToArray() } function Find-PSResourceDependencies { <# .SYNOPSIS Resolves all dependencies (RequiredModules + ModuleList) from a .psd1 manifest via remote repository. .EXAMPLE Find-PSResourceDependencies -ManifestPath "./MyModule/MyModule.psd1" .OUTPUTS Array of PSCustomObject with Name, Version, Repository, and IsRedirected properties. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ManifestPath, [Parameter(Mandatory = $false)] [string]$RedirectMapPath, [Parameter(Mandatory = $false)] [string]$Repository, [Parameter(Mandatory = $false)] [PSCredential]$Credential, [Parameter(Mandatory = $false)] [switch]$Prerelease ) if (-not (Test-Path $ManifestPath)) { throw "Manifest file not found: $ManifestPath" } $resolvedPath = Resolve-Path $ManifestPath if (-not $resolvedPath.Path.EndsWith('.psd1')) { throw "File must be a PowerShell module manifest (.psd1): $ManifestPath" } Write-Verbose "Reading manifest from: $resolvedPath" $manifest = Import-PowerShellDataFile -Path $resolvedPath $moduleDependencies = @(Get-ManifestModuleDependencies -Manifest $manifest) if ($moduleDependencies.Count -eq 0) { Write-Verbose "No module dependencies found in manifest (RequiredModules or ModuleList)" return @() } $manifestModuleName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath.Path) $manifestModuleVersion = if ($manifest.ModuleVersion) { $manifest.ModuleVersion.ToString() } else { "" } if ($RedirectMapPath) { if (-not (Test-Path $RedirectMapPath)) { throw "Redirect map file not found: $RedirectMapPath" } Write-Verbose "Loading redirect map from: $RedirectMapPath" $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable } else { Write-Verbose "Looking for redirect map based on manifest: $manifestModuleName version $manifestModuleVersion" $redirectMap = Get-MergedRedirectMap -OuterMap $script:defaultRedirectMap -Name $manifestModuleName -Version $manifestModuleVersion } Write-Verbose "Found $($moduleDependencies.Count) module dependency(ies) in manifest" $dependencyGraph = @{} foreach ($depEntry in $moduleDependencies) { $moduleName = $depEntry.Name $moduleVersion = $depEntry.Version $mergedRedirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $moduleName -Version ($moduleVersion ?? "") $redirectResult = Find-DependencyRedirect -DependencyName $moduleName -DependencyVersion $moduleVersion ` -RedirectMap $mergedRedirectMap -Indent "" Build-RemoteDependencyGraph -ModuleName $redirectResult.ResolvedName -ModuleVersion $redirectResult.ResolvedVersion ` -Graph $dependencyGraph -RedirectMap $mergedRedirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease } Resolve-DiamondDependencies -Graph $dependencyGraph $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph) $resolvedDependencies = [System.Collections.ArrayList]@() foreach ($moduleKey in $topologicalOrder) { $node = $dependencyGraph[$moduleKey] [void]$resolvedDependencies.Add([PSCustomObject]@{ Name = $node.Name Version = $node.Version Repository = $node.Repository IsRedirected = $false # Graph already has redirected versions applied }) } Write-Verbose "Resolved $($resolvedDependencies.Count) module(s) (including transitive dependencies)" return $resolvedDependencies.ToArray() } function Install-PSResourceDependencies { <# .SYNOPSIS Installs all manifest dependencies using Find-PSResourceDependencies. .EXAMPLE Install-PSResourceDependencies -ManifestPath "./MyModule/MyModule.psd1" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ManifestPath, [Parameter(Mandatory = $false)] [string]$RedirectMapPath, [Parameter(Mandatory = $false)] [ValidateSet('CurrentUser', 'AllUsers')] [string]$Scope = 'CurrentUser', [Parameter(Mandatory = $false)] [string]$Repository, [Parameter(Mandatory = $false)] [PSCredential]$Credential, [Parameter(Mandatory = $false)] [switch]$Force ) $findParams = @{ ManifestPath = $ManifestPath } if ($RedirectMapPath) { $findParams['RedirectMapPath'] = $RedirectMapPath } if ($Repository) { $findParams['Repository'] = $Repository } if ($Credential) { $findParams['Credential'] = $Credential } $resolvedDependencies = Find-PSResourceDependencies @findParams if (-not $resolvedDependencies -or $resolvedDependencies.Count -eq 0) { Write-Verbose "No dependencies to install" return } Write-Verbose "Installing $($resolvedDependencies.Count) resolved dependency(ies)" foreach ($dependency in $resolvedDependencies) { $installed = $null if (-not $Force) { $installed = Get-PSResource -Name $dependency.Name -ErrorAction SilentlyContinue | Where-Object { $_.Version.ToString() -eq $dependency.Version } } if (-not $installed) { Write-Host "Installing dependency: $($dependency.Name) version $($dependency.Version)" $installParams = @{ Name = $dependency.Name Version = $dependency.Version Scope = $Scope TrustRepository = $true SkipDependencyCheck = $true } if ($Repository) { $installParams['Repository'] = $Repository } if ($Credential) { $installParams['Credential'] = $Credential } if ($Force) { $installParams['Reinstall'] = $true } Install-PSResource @installParams } else { Write-Verbose "Already installed: $($dependency.Name) version $($dependency.Version)" } } Write-Host "Successfully installed all dependencies from manifest" } function Import-PSResourceDependencies { <# .SYNOPSIS Imports all manifest dependencies (RequiredModules + ModuleList) in topological order with pinned versions. Prevents "assembly already loaded" errors from incomplete graphs. .EXAMPLE Import-PSResourceDependencies -ManifestPath "./MyModule/MyModule.psd1" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ManifestPath, [Parameter(Mandatory = $false)] [string]$RedirectMapPath, [Parameter(Mandatory = $false)] [switch]$Force, [Parameter(Mandatory = $false)] [switch]$PassThru ) # Validate manifest path if (-not (Test-Path $ManifestPath)) { throw "Manifest file not found: $ManifestPath" } $resolvedPath = Resolve-Path $ManifestPath if (-not $resolvedPath.Path.EndsWith('.psd1')) { throw "File must be a PowerShell module manifest (.psd1): $ManifestPath" } Write-Verbose "Reading manifest from: $resolvedPath" # Parse the manifest $manifest = Import-PowerShellDataFile -Path $resolvedPath # Extract module dependencies from both RequiredModules and ModuleList $moduleDependencies = @(Get-ManifestModuleDependencies -Manifest $manifest) if ($moduleDependencies.Count -eq 0) { Write-Verbose "No module dependencies found in manifest (RequiredModules or ModuleList)" return } $manifestModuleName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath.Path) $manifestModuleVersion = if ($manifest.ModuleVersion) { $manifest.ModuleVersion.ToString() } else { "" } if ($RedirectMapPath) { if (-not (Test-Path $RedirectMapPath)) { throw "Redirect map file not found: $RedirectMapPath" } Write-Verbose "Loading redirect map from: $RedirectMapPath" $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable } else { Write-Verbose "Looking for redirect map based on manifest: $manifestModuleName version $manifestModuleVersion" $redirectMap = Get-MergedRedirectMap -OuterMap $script:defaultRedirectMap -Name $manifestModuleName -Version $manifestModuleVersion } Write-Verbose "Found $($moduleDependencies.Count) module dependency(ies) in manifest" $dependencyGraph = @{} foreach ($depEntry in $moduleDependencies) { $moduleName = $depEntry.Name $moduleVersion = $depEntry.Version $mergedRedirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $moduleName -Version ($moduleVersion ?? "") $redirectResult = Find-DependencyRedirect -DependencyName $moduleName -DependencyVersion $moduleVersion ` -RedirectMap $mergedRedirectMap -Indent "" Build-InstalledDependencyGraph -ModuleName $redirectResult.ResolvedName -ModuleVersion $redirectResult.ResolvedVersion ` -Graph $dependencyGraph -RedirectMap $mergedRedirectMap } Resolve-DiamondDependencies -Graph $dependencyGraph # Compute topological order Write-Verbose "Computing topological order" $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph) Write-Verbose "Import order ($($topologicalOrder.Count) modules):" for ($i = 0; $i -lt $topologicalOrder.Count; $i++) { Write-Verbose " $($i + 1). $($topologicalOrder[$i])" } Write-Verbose "Pre-loading all modules in topological order" $importedModules = @{} foreach ($moduleKey in $topologicalOrder) { $node = $dependencyGraph[$moduleKey] $modName = $node.Name $modVersion = $node.Version $loadedModule = Get-Module -Name $modName | Where-Object { $_.Version.ToString() -eq $modVersion } if ($loadedModule -and -not $Force) { Write-Verbose "Already loaded: $modName version $modVersion" $importedModules[$moduleKey] = $loadedModule continue } # -Global ensures modules persist after this function returns $importParams = @{ Name = $modName RequiredVersion = $modVersion ErrorAction = 'Stop' DisableNameChecking = $true Global = $true } if ($Force) { $importParams['Force'] = $true } try { Write-Verbose "Importing: $modName version $modVersion" $imported = Import-Module @importParams -PassThru $importedModules[$moduleKey] = $imported } catch { throw "Failed to import $modName version $($modVersion): $_" } } Write-Verbose "Successfully imported $($importedModules.Count) module(s) from manifest" if ($PassThru) { return $importedModules.Values } } function Import-ModulePinned { <# .SYNOPSIS Imports a module after pre-loading ALL transitive dependencies at exact versions. Prevents PowerShell from loading wrong versions via minimum-version semantics. .EXAMPLE Import-ModulePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0" #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name, [Parameter(Mandatory = $true, Position = 1)] [string]$RequiredVersion, [Parameter(Mandatory = $false)] [string]$RedirectMapPath, [Parameter(Mandatory = $false)] [switch]$Force, [Parameter(Mandatory = $false)] [string]$Prefix, [Parameter(Mandatory = $false)] [switch]$PassThru ) # Load redirect map if ($RedirectMapPath) { if (-not (Test-Path $RedirectMapPath)) { throw "Redirect map file not found: $RedirectMapPath" } Write-Verbose "Loading redirect map from: $RedirectMapPath" $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable } else { Write-Verbose "Using default redirect map" $redirectMap = $script:defaultRedirectMap } $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion Write-Verbose "Building dependency graph for $Name version $RequiredVersion" $dependencyGraph = @{} Build-InstalledDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion ` -Graph $dependencyGraph -RedirectMap $redirectMap Resolve-DiamondDependencies -Graph $dependencyGraph Write-Verbose "Dependency graph contains $($dependencyGraph.Count) modules:" foreach ($nodeKey in $dependencyGraph.Keys | Sort-Object) { $node = $dependencyGraph[$nodeKey] Write-Verbose " $nodeKey" Write-Verbose " Location: $($node.InstalledLocation)" if ($node.Dependencies.Count -gt 0) { Write-Verbose " Dependencies:" foreach ($dep in $node.Dependencies) { Write-Verbose " -> $dep" } } else { Write-Verbose " Dependencies: (none)" } } # Compute topological order Write-Verbose "Computing topological order" $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph) Write-Verbose "Import order ($($topologicalOrder.Count) modules):" for ($i = 0; $i -lt $topologicalOrder.Count; $i++) { Write-Verbose " $($i + 1). $($topologicalOrder[$i])" } Write-Verbose "Pre-loading all modules in topological order" $importedModules = @{} foreach ($moduleKey in $topologicalOrder) { $node = $dependencyGraph[$moduleKey] $modName = $node.Name $modVersion = $node.Version $loadedModule = Get-Module -Name $modName | Where-Object { $_.Version.ToString() -eq $modVersion } if ($loadedModule -and -not $Force) { Write-Verbose "Already loaded: $modName version $modVersion" $importedModules[$moduleKey] = $loadedModule continue } # -Global ensures modules persist after this function returns $importParams = @{ Name = $modName RequiredVersion = $modVersion ErrorAction = 'Stop' DisableNameChecking = $true Global = $true } if ($Force) { $importParams['Force'] = $true } try { Write-Verbose "Importing: $modName version $modVersion" $imported = Import-Module @importParams -PassThru $importedModules[$moduleKey] = $imported } catch { throw "Failed to import $modName version $($modVersion): $_" } } Write-Verbose "Returning main module" $mainModuleKey = "${Name}@${RequiredVersion}" $mainModule = $importedModules[$mainModuleKey] if (-not $mainModule) { $mainModule = Get-Module -Name $Name | Where-Object { $_.Version.ToString() -eq $RequiredVersion } } Write-Verbose "Successfully imported $Name version $RequiredVersion (and $($importedModules.Count - 1) dependencies)" if ($PassThru) { return $mainModule } } function Find-PSResourcesPinned { <# .SYNOPSIS Resolves a module and all dependencies with pinned versions. Returns results in topological order. .EXAMPLE Find-PSResourcesPinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0" .OUTPUTS Array of objects with Name, Version, Repository, and Dependencies properties. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$RequiredVersion, [Parameter(Mandatory = $false)] [string]$RedirectMapPath, [Parameter(Mandatory = $false)] [string]$Repository, [Parameter(Mandatory = $false)] [PSCredential]$Credential, [Parameter(Mandatory = $false)] [switch]$Prerelease ) # Load redirect map if ($RedirectMapPath) { if (-not (Test-Path $RedirectMapPath)) { throw "Redirect map file not found: $RedirectMapPath" } Write-Verbose "Loading redirect map from: $RedirectMapPath" $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable } else { Write-Verbose "Using default redirect map" $redirectMap = $script:defaultRedirectMap } $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion Write-Verbose "Building dependency graph for $Name version $RequiredVersion" $dependencyGraph = @{} Build-RemoteDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion ` -Graph $dependencyGraph -RedirectMap $redirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease Resolve-DiamondDependencies -Graph $dependencyGraph Write-Verbose "Computing topological order" $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph) $resolvedModules = [System.Collections.ArrayList]@() foreach ($moduleKey in $topologicalOrder) { $node = $dependencyGraph[$moduleKey] [void]$resolvedModules.Add([PSCustomObject]@{ Name = $node.Name Version = $node.Version Repository = $node.Repository Dependencies = $node.Dependencies }) } Write-Verbose "Found $($resolvedModules.Count) module(s) (including main module and all dependencies)" return $resolvedModules.ToArray() } # SIG # Begin signature block # MIIncQYJKoZIhvcNAQcCoIInYjCCJ14CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCClPUYI3amuSDUy # 5Qku568MOQ61BfA/2vvnfH+KzZ54fKCCDMkwggYEMIID7KADAgECAhMzAAACHPrN # xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD # b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1 # OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD # VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB # DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP # oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC # /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf # rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j # qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT # xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B # Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O # BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL # EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT # DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw # YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy # bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl # MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC # AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN # rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK # 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK # Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY # BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu # uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE # msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz # 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6 # U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO # 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD # 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC # EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS # b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX # DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ # Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq # lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo # 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv # QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a # 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1 # FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO # GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7 # ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ # uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS # CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm # VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3 # SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E # BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX # LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB # Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP # oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw # TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC # AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D # 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY # nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI # vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6 # aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w # PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7 # RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK # /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK # YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw # YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT # Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghn+MIIZ+gIBATBu # MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc # +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwG # CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI # hvcNAQkEMSIEIDYKto4bhQZFVWLCB2PEp4wQCmp8kBp26HqVegSZIl1pMEIGCisG # AQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAyh6/zL8WlyWPsgbnivlJ # aIULErR5mx4vzkVznNyUtDF0eiHnGzJkG7TRFm2UlTdST4/sg3NfF26mMhUYlqal # ds+FWpqFT4IT9GgLgv3ArMBzEL0AVZvyHjGA86cCDMtDrTTVrJHTLRP9HbHeGpty # Zb9HhcOK9vfz83gvwYQe3kdR6RjrUZjNCi1BVLNbKhTm8lawPe6s2gM0IUK/0KzM # l+TAJRrt+Hn4nUl+D14G5/tQkBA6r++5rbTEzSP/Pe+KcXL3D5q/2bpQaCQGI56B # vOKsB/Hp1z1I90DWM1vClEyHrv3wXhnw8JN0TYZYox5vVENevNI2PBJ+yQbRE1zV # BaGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kwgheF # AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIB # QQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCCl502lC4sgoQLwzBer # Voe7GQQIf9JpSUfUEofOB7entgIGaeuLMYcPGBMyMDI2MDUyMTIxMDMwNy40MzVa # MASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0 # ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyRDFBLTA1RTAtRDk0NzElMCMG # A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIFEKAD # AgECAhMzAAACEtEIBjzKGE+qAAEAAAISMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m # dCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgxNVoXDTI2MTExMzE4 # NDgxNVowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTAr # BgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUG # A1UECxMeblNoaWVsZCBUU1MgRVNOOjJEMUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxN # aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEAr0zToDkpWQtsZekS0cV0quDdKSTGkovvBaZH0OAIEi0O3CcO # 77JiX8c4Epq9uibHVZZ1W/LoufE172vkRXO+QYNtWWorECJ2AcZQ10bpAltkhZNi # XlVJ8L3QzhKgrXrmMkm2J+/g81U23JPcO4wXHEftonT3wpd//936rjmwxMm7Nkbs # ygbJf+4AVBMNr4aMPQhBd76od0KMB6WrvyEGOOU0893OFufS5EDey4n44WgaxJE0 # Vnv3/OOvuOw5Kp1KPqjjYJ+L9ywLuBMtcDfLpNQO/h1eFEoMrbiEM67TOfNlXfxb # Dz4MlsYvLioxgd2Xzey1QxrV1+i+JyVDJMiSe9gKOuzpiQQFE19DUPgsidyjLTzX # EhSVLBlRor0eCVf7gC6Rfk8NY3rO2sggOL79vU5FuDKTh/sIOtcUHeHC42jBGB+t # fdKC1KOBR+UlN9aOzg8mpUNI2FgqQvirVP9ppbeMUfvp2wA9voyTiRWvDgzCxo8x # lJ1nscYTHIQrmkF9j/Ca0IDmt8fvOn64nnlJOGUYZYHMC1l0xtgkYTE1ESUqqkaw # Kk7iqbxdnLyycS+dR+zaxPudMDLrQFz8lgfy9obk0D8HC2dzhWpYNn5hdkoPEzgC # qQUOp8v3Qj/sd4anyupe5KoCkjABOP3yhSQ4W9Z+DrJnhM/rbsXC7oTv26cCAwEA # AaOCAUkwggFFMB0GA1UdDgQWBBRSBblSxb5cYKYOwvd/VfoXOfu33jAfBgNVHSME # GDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1l # LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsG # AQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p # Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMB # Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDAN # BgkqhkiG9w0BAQsFAAOCAgEAXnSAkmX79Rc7lxS1wOozXJ7V0ou5DntVcOJplIkD # jvEN8BIQph4U+gSOLZuVReP/z9YdUiUkcPwL1PM245/kEX1EegpxNc8HDA6hKCHg # 0ALNEcuxnGOlgKLokXfUer1D5hiW8PABM9R+neiteTgPaaRlJFvGTYvotc0uqGiE # S5hMQhL8RNFhpS9RcIWHtnQGEnrdOUvCAhs4FeViawcmLTKv+1870c/MeTHi0QDd # eR+7/Wg4qhkJ2k1iEHJdmYf8rIV0NRBZcdRTTdHee35SXP5neNCfAkjDIuZycRud # 6jzPLCNLiNYzGXBswzJygj4EeSORT7wMvaFuKeRAXoXC3wwYvgIsI1zn3DGY625Y # +yZSi8UNSNHuri36Zv9a+Q4vJwDpYK36S0TB2pf7xLiiH32nk7YK73Rg98W6fZ2I # NuzYzZ7Ghgvfffkj4EUXg1E0EffY1pEqkbpDTP7h/DBqtzoPXsyw2MUh+7yvWcq2 # BGZSuca6CY6X4ioMuc5PWpsmvOOli7ARNA7Ab8kKdCc2gNDLacglsweZEc9/VQB6 # hls/b6Kk32nkwuHExKlaeoSVrKB5U9xlp1+c8J/7GJj4Rw7AiQ8tcp+WmfyD8KxX # 2QlKbDi4SUjnglv4617R8+a/cDWJyaMt8279Wn7f2yMedN7kfGIQ5SZj66RdhdlZ # Oq8wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEB # CwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD # VQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAe # Fw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGm # TOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/H # ZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDc # wUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62A # W36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1w # jjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCG # MFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ # 1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP # 8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFz # ymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHz # NgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3 # xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsG # AQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/ # LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEG # DCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYB # BQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8G # A1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQw # VgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j # cmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUF # BwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br # aS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQEL # BQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfC # cTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AF # vonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l # 9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn # 8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5m # O0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyx # TkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4 # S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9 # y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM # +Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhw # RNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCCAkEC # AQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0 # ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyRDFBLTA1RTAtRDk0NzElMCMG # A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIa # AxUA5VHBr4h00EN7jUdQ33SE+qbk/8CggYMwgYCkfjB8MQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1T # dGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO25oDgwIhgPMjAyNjA1MjEx # NTE2MDhaGA8yMDI2MDUyMjE1MTYwOFowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA # 7bmgOAIBADAKAgEAAgIdOgIB/zAHAgEAAgISnjAKAgUA7brxuAIBADA2BgorBgEE # AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag # MA0GCSqGSIb3DQEBCwUAA4IBAQA0iIbhMu4oDtcy7JlKbJyW7o1Iz8Pmq9Y3f3/Q # PkS68XE/ab3NAgHB3Y+r90UoXBM6AYC9lfx3X9uwgPaJi1g3kwamBSjWzhMuzI7w # ZsygXB06QRm/MqAOHStDmMYe4LnoYOfm+IafEWcOuHjwWOKkOO0avffn4Cuj/P4C # Ju0oIL9OytSJ5BcIy/W8hBDzp1PDBG2PVBaRqzOKsYxnKzMIAHcjFOL47plWLmIG # VgqMY9qdw+9naxe1JD4f1++1SERCzgzQD7KNqTYfh4d2hYtlOskZV1G9bvL60+Lg # Lr4X8o2mJik2KbciERPQ3rOC0J/j5dCt1pvsnAFBl3tmN7Y7MYIEDTCCBAkCAQEw # gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIS0QgGPMoYT6oA # AQAAAhIwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B # CRABBDAvBgkqhkiG9w0BCQQxIgQgbNXlchNRkMt1Pbjh+nORzeXV5XTwiv3ll5xB # 8LtVM1UwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBz+X5GvO7WngknH4BZ # eYU+BzBL1Jy5oJ8wVlTNIxfYgzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAACEtEIBjzKGE+qAAEAAAISMCIEIBrOq4Vc0OrwzfnoFIc3 # rgaGJq0TiQTWnWJQmaeCpDSkMA0GCSqGSIb3DQEBCwUABIICAEX1W5Jo4VayagdG # Gb4wfDwwW3Hft3cpmrPbQH/4qe21mouDl0zejucFbb4SBHRvhatfdybpd6YMTJyf # RlSh5ww2XuELAazD/T4TcYJMCF21YVwdIHueoLBG2BqsJCIWS9+FIZFXcb/6+Nvb # /Jhcj9+wvRWCUR4kDUzsYs/bfBNhPRRo1gGhn47gWNuKEyX6Zw2dBORYsuOlcRpN # t0Q4vIH8PbIisoOvMHaYjqtJmsxf6hKDe1a+fTsbAnWXrdzGkN9kOeCJ3fjMW9zw # mw1Cl875kVUkcAfFDbtLDzl5q1KZC7spzhyn+L7COkwjb2heZDT0SHFzhZ71AFjN # tQC41vNpz34AxQWZsZeLFqjlUbNYwB71grnqqvYe9kW1Cp1CWdyxU1Ud7A9W8NQt # 2tMMl0aEKwUQjEUTjDcHfyxDpAkmcjlJUw8BMPWiLnDOirAGPPi2ah95sQeP2nIj # CbL6jh07hypWqiI2X5mVgDLFK33mz2FCW5iWht2o+IqVjyY6o1+ygtPi8F+RFNU5 # W+7WkFGbjR28lfs3z6G1iqelbNu2IZu+ZZgfer98SSsxVjSAWw4+O9ZCzO21swN/ # n+GjCJFdU/tCh8FBpK3FPkcARYN/8ROSh21PYmrjJWlsjPcgJ0M7N7nqZWKtfF7h # iarCBLBbL25kw92GkbTZhvKoGw5y # SIG # End signature block |