Public/Get/Get-TechToolboxConfig.ps1
|
function Get-TechToolboxConfig { <# .SYNOPSIS Loads and validates a TechToolbox JSON configuration file. .DESCRIPTION Reads a JSON configuration file from disk, converts it into a hashtable-based object graph, validates the required top-level and password-default settings, and returns the parsed configuration data. The function maintains an in-memory script-scoped cache keyed by the resolved file path and the file's LastWriteTimeUtc value. If the same file is requested again and has not changed, the cached configuration is returned instead of re-reading and re-parsing the JSON. Behavior details: - Throws if the target file does not exist. - Throws if the file is empty or contains invalid JSON. - Uses ConvertFrom-Json -AsHashtable on PowerShell 7+. - Uses a recursive PSCustomObject-to-hashtable fallback on Windows PowerShell 5.1. - Validates that schemaVersion and settings exist at the top level. - Validates that settings.passwords.default contains separator, style, length, and digits. - Preserves raw values exactly as stored in the JSON and does not trim or mutate configuration values. .PARAMETER Path The path to the TechToolbox JSON configuration file to load. The file must exist and contain valid JSON with the required TechToolbox configuration structure. .INPUTS None. You cannot pipe objects to Get-TechToolboxConfig. .OUTPUTS System.Collections.Hashtable. Returns the parsed configuration as a nested hashtable/array structure. .EXAMPLE Get-TechToolboxConfig -Path '.\Config\config.json' Loads the main TechToolbox configuration file and returns the parsed settings. .EXAMPLE $config = Get-TechToolboxConfig -Path '$env:TT_ModuleRoot\Config\config.json' $config.settings.passwords.default Loads the configuration and inspects the default password settings. .EXAMPLE Get-TechToolboxConfig -Path '.\Config\config.json' Get-TechToolboxConfig -Path '.\Config\config.json' Demonstrates repeated calls for the same unchanged file. The second call can return the cached configuration data. .NOTES Updates both the private script-scoped cache variable and the legacy $script:TechToolboxConfig variable for compatibility with existing module code. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Path ) # --- PS5-compatible deep converter (only used on PS5) --- function ConvertTo-Hashtable { param([Parameter(Mandatory)]$InputObject) if ($InputObject -is [hashtable]) { return $InputObject } if ($InputObject -is [System.Collections.IDictionary]) { $ht = @{} foreach ($k in $InputObject.Keys) { $ht[$k] = ConvertTo-Hashtable $InputObject[$k] } return $ht } if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { return @( $InputObject | ForEach-Object { ConvertTo-Hashtable $_ } ) } if ($InputObject -is [pscustomobject]) { $ht = @{} foreach ($p in $InputObject.PSObject.Properties) { $ht[$p.Name] = ConvertTo-Hashtable $p.Value } return $ht } return $InputObject } function Get-TechToolboxPathRoots { param([Parameter(Mandatory)][string]$ConfigFilePath) $oneDriveRoot = $env:OneDriveCommercial if (-not $oneDriveRoot) { $oneDriveRoot = $env:OneDrive } if (-not $oneDriveRoot) { $oneDriveRoot = $env:OneDriveConsumer } if (-not $oneDriveRoot) { $oneDriveRoot = $env:USERPROFILE } $ttHome = if ($env:TT_Home) { $env:TT_Home } else { Join-Path $oneDriveRoot 'TechStuff\TechToolbox' } $defaultModuleRoot = Split-Path -Parent (Split-Path -Parent $ConfigFilePath) $moduleRoot = if ($env:TT_ModuleRoot) { $env:TT_ModuleRoot } else { $defaultModuleRoot } $logsRoot = if ($env:TT_LogsRoot) { $env:TT_LogsRoot } else { Join-Path (Join-Path $ttHome 'LogsAndExports') 'Logs' } $exportsRoot = if ($env:TT_ExportsRoot) { $env:TT_ExportsRoot } else { Join-Path (Join-Path $ttHome 'LogsAndExports') 'Exports' } return @{ ModuleRoot = $moduleRoot.TrimEnd('\\') LogsRoot = $logsRoot.TrimEnd('\\') ExportsRoot = $exportsRoot.TrimEnd('\\') HomeRoot = $ttHome.TrimEnd('\\') } } function Assert-AbsolutePathPrefix { param( [string]$Value, [Parameter(Mandatory)][string]$FromRoot, [Parameter(Mandatory)][string]$ToRoot ) if ([string]::IsNullOrEmpty($Value)) { return $Value } $from = $FromRoot.TrimEnd('\\') $to = $ToRoot.TrimEnd('\\') if ([string]::IsNullOrWhiteSpace($from) -or [string]::IsNullOrWhiteSpace($to)) { return $Value } if ($Value.StartsWith($from, [System.StringComparison]::OrdinalIgnoreCase)) { $suffix = $Value.Substring($from.Length).TrimStart('\\', '/') if ([string]::IsNullOrWhiteSpace($suffix)) { return $to } return (Join-Path $to $suffix) } return $Value } function Resolve-TechToolboxConfigNode { param( $Node, [Parameter(Mandatory)][hashtable]$PathRoots ) if ($null -eq $Node) { return $null } if ($Node -is [hashtable]) { foreach ($k in @($Node.Keys)) { $Node[$k] = Resolve-TechToolboxConfigNode -Node $Node[$k] -PathRoots $PathRoots } return $Node } if ($Node -is [System.Collections.IList]) { for ($i = 0; $i -lt $Node.Count; $i++) { $Node[$i] = Resolve-TechToolboxConfigNode -Node $Node[$i] -PathRoots $PathRoots } return $Node } if ($Node -is [string]) { $resolved = [Environment]::ExpandEnvironmentVariables($Node) $resolved = $resolved.Replace('%TT_Home%', $PathRoots.HomeRoot) $resolved = $resolved.Replace('%TT_ModuleRoot%', $PathRoots.ModuleRoot) $resolved = $resolved.Replace('%TT_LogsAndExportsRoot%', (Join-Path $PathRoots.HomeRoot 'LogsAndExports')) $resolved = $resolved.Replace('%TT_LogsRoot%', $PathRoots.LogsRoot) $resolved = $resolved.Replace('%TT_ExportsRoot%', $PathRoots.ExportsRoot) $resolved = Assert-AbsolutePathPrefix -Value $resolved -FromRoot 'C:\TechToolbox_LogsAndExports\Logs' -ToRoot $PathRoots.LogsRoot $resolved = Assert-AbsolutePathPrefix -Value $resolved -FromRoot 'C:\TechToolbox_LogsAndExports\Exports' -ToRoot $PathRoots.ExportsRoot $resolved = Assert-AbsolutePathPrefix -Value $resolved -FromRoot 'C:\TechToolbox' -ToRoot $PathRoots.ModuleRoot return $resolved } return $Node } function Merge-TechToolboxConfigNode { param( [Parameter(Mandatory)]$Base, [Parameter(Mandatory)]$Override ) if ($Override -is [hashtable] -and $Base -is [hashtable]) { foreach ($key in $Override.Keys) { if ($Base.ContainsKey($key)) { $Base[$key] = Merge-TechToolboxConfigNode -Base $Base[$key] -Override $Override[$key] } else { $Base[$key] = $Override[$key] } } return $Base } # Arrays and scalar values are replaced wholesale by the override value. return $Override } function Get-TechToolboxSecretsOverride { param([Parameter(Mandatory)][string]$ConfigFilePath) if ($env:TT_DisableConfigSecretsMerge -eq '1') { return $null } $configDir = Split-Path -Parent $ConfigFilePath $secretsPath = if ([string]::IsNullOrWhiteSpace($env:TT_ConfigSecretsPath)) { Join-Path $configDir 'config.secrets.json' } else { [Environment]::ExpandEnvironmentVariables($env:TT_ConfigSecretsPath) } if (-not (Test-Path -LiteralPath $secretsPath)) { return $null } $raw = Get-Content -LiteralPath $secretsPath -Raw -Encoding UTF8 -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($raw)) { return $null } if ($PSVersionTable.PSVersion.Major -ge 7) { return (ConvertFrom-Json -InputObject $raw -AsHashtable) } return (ConvertTo-Hashtable (ConvertFrom-Json -InputObject $raw)) } if (-not (Test-Path -LiteralPath $Path)) { throw "Get-TechToolboxConfig: config file not found at '$Path'." } $fi = Get-Item -LiteralPath $Path $pathRoots = Get-TechToolboxPathRoots -ConfigFilePath $Path $pathFingerprint = '{0}|{1}|{2}|{3}' -f $pathRoots.ModuleRoot, $pathRoots.LogsRoot, $pathRoots.ExportsRoot, $pathRoots.HomeRoot $secretsPath = if ([string]::IsNullOrWhiteSpace($env:TT_ConfigSecretsPath)) { Join-Path (Split-Path -Parent $fi.FullName) 'config.secrets.json' } else { [Environment]::ExpandEnvironmentVariables($env:TT_ConfigSecretsPath) } $secretsLastWrite = '' if (Test-Path -LiteralPath $secretsPath) { $secretsLastWrite = (Get-Item -LiteralPath $secretsPath).LastWriteTimeUtc.ToString('o') } $secretsMergeFlag = if ($env:TT_DisableConfigSecretsMerge -eq '1') { 'off' } else { 'on' } $cacheFingerprint = '{0}|{1}|{2}|{3}' -f $pathFingerprint, $secretsPath, $secretsLastWrite, $secretsMergeFlag # --- Simple session cache (StrictMode-safe) --- if (-not (Get-Variable -Name __cfgCache -Scope Script -ErrorAction SilentlyContinue)) { $script:__cfgCache = $null } $cache = $script:__cfgCache if ($cache -and $cache.Path -eq $fi.FullName -and $cache.LastWriteTimeUtc -eq $fi.LastWriteTimeUtc -and $cache.PathFingerprint -eq $cacheFingerprint) { return $cache.Data } # --- Load & parse (use -Raw; DO NOT use $raw variable) --- $jsonRaw = Get-Content -LiteralPath $fi.FullName -Raw -ErrorAction Stop if ($PSVersionTable.PSVersion.Major -ge 7) { $data = ConvertFrom-Json -InputObject $jsonRaw -AsHashtable } else { $data = ConvertTo-Hashtable (ConvertFrom-Json -InputObject $jsonRaw) } $secretsOverride = Get-TechToolboxSecretsOverride -ConfigFilePath $fi.FullName if ($secretsOverride) { if ($secretsOverride.ContainsKey('settings') -and $secretsOverride.settings -is [hashtable]) { if (-not ($data.settings -is [hashtable])) { $data.settings = @{} } $data.settings = Merge-TechToolboxConfigNode -Base $data.settings -Override $secretsOverride.settings } if ($secretsOverride.ContainsKey('paths') -and $secretsOverride.paths -is [hashtable]) { if (-not ($data.paths -is [hashtable])) { $data.paths = @{} } $data.paths = Merge-TechToolboxConfigNode -Base $data.paths -Override $secretsOverride.paths } } $data = Resolve-TechToolboxConfigNode -Node $data -PathRoots $pathRoots # --- Update cache --- $script:__cfgCache = @{ Path = $fi.FullName LastWriteTimeUtc = $fi.LastWriteTimeUtc PathFingerprint = $cacheFingerprint Data = $data } # Optional: maintain the legacy global if other code expects it $script:TechToolboxConfig = $data return $data } # SIG # Begin signature block # MIIfAgYJKoZIhvcNAQcCoIIe8zCCHu8CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBXFOJcVtUefDEy # l5tTnjjQwTQSsUxDrRZ1mKuU4Lz81aCCGEowggUMMIIC9KADAgECAhAR+U4xG7FH # qkyqS9NIt7l5MA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNVBAMME1ZBRFRFSyBDb2Rl # IFNpZ25pbmcwHhcNMjUxMjE5MTk1NDIxWhcNMjYxMjE5MjAwNDIxWjAeMRwwGgYD # VQQDDBNWQURURUsgQ29kZSBTaWduaW5nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEA3pzzZIUEY92GDldMWuzvbLeivHOuMupgpwbezoG5v90KeuN03S5d # nM/eom/PcIz08+fGZF04ueuCS6b48q1qFnylwg/C/TkcVRo0WFcKoFGT8yGxdfXi # caHtapZfbSRh73r7qR7w0CioVveNBVgfMsTgE0WKcuwxemvIe/ptmkfzwAiw/IAC # Ib0E0BjiX4PySbwWy/QKy/qMXYY19xpRItVTKNBtXzADUtzPzUcFqJU83vM2gZFs # Or0MhPvM7xEVkOWZFBAWAubbMCJ3rmwyVv9keVDJChhCeLSz2XR11VGDOEA2OO90 # Y30WfY9aOI2sCfQcKMeJ9ypkHl0xORdhUwZ3Wz48d3yJDXGkduPm2vl05RvnA4T6 # 29HVZTmMdvP2475/8nLxCte9IB7TobAOGl6P1NuwplAMKM8qyZh62Br23vcx1fXZ # TJlKCxBFx1nTa6VlIJk+UbM4ZPm954peB/fIqEacm8LkZ0cPwmLE5ckW7hfK4Trs # o+RaudU1sKeA+FvpOWgsPccVRWcEYyGkwbyTB3xrIBXA+YckbANZ0XL7fv7x29hn # gXbZipGu3DnTISiFB43V4MhNDKZYfbWdxze0SwLe8KzIaKnwlwRgvXDMwXgk99Mi # EbYa3DvA/5ZWikLW9PxBFD7Vdr8ZiG/tRC9I2Y6fnb+PVoZKc/2xsW0CAwEAAaNG # MEQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQW # BBRfYLVE8caSc990rnrIHUjoB7X/KjANBgkqhkiG9w0BAQsFAAOCAgEAiGB2Wmk3 # QBtd1LcynmxHzmu+X4Y5DIpMMNC2ahsqZtPUVcGqmb5IFbVuAdQphL6PSrDjaAR8 # 1S8uTfUnMa119LmIb7di7TlH2F5K3530h5x8JMj5EErl0xmZyJtSg7BTiBA/UrMz # 6WCf8wWIG2/4NbV6aAyFwIojfAcKoO8ng44Dal/oLGzLO3FDE5AWhcda/FbqVjSJ # 1zMfiW8odd4LgbmoyEI024KkwOkkPyJQ2Ugn6HMqlFLazAmBBpyS7wxdaAGrl18n # 6bS7QuAwCd9hitdMMitG8YyWL6tKeRSbuTP5E+ASbu0Ga8/fxRO5ZSQhO6/5ro1j # PGe1/Kr49Uyuf9VSCZdNIZAyjjeVAoxmV0IfxQLKz6VOG0kGDYkFGskvllIpQbQg # WLuPLJxoskJsoJllk7MjZJwrpr08+3FQnLkRuisjDOc3l4VxFUsUe4fnJhMUONXT # Sk7vdspgxirNbLmXU4yYWdsizz3nMUR0zebUW29A+HYme16hzrMPOeyoQjy4I5XX # 3wXAFdworfPEr/ozDFrdXKgbLwZopymKbBwv6wtT7+1zVhJXr+jGVQ1TWr6R+8ea # tIOFnY7HqGaxe5XB7HzOwJKdj+bpHAfXft1vUoiKr16VajLigcYCG8MdwC3sngO3 # JDyv2V+YMfsYBmItMGBwvizlQ6557NbK95EwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqG # SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYg # MjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphB # cr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6p # vF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHe # HYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEd # gkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjU # jsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bR # VFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeS # LsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIV # NSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL # 6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2Zd # SoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFU # eEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw # DQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/ # T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQ # E7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9r # EVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y # 1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gx # dEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3t # y9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcy # tL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEB # YTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud # /v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiS # uEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZP # ubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsF # ADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV # BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB # MjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzEL # MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE # aWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUg # MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMr # V7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8 # dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7M # rxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZ # ZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFO # nHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+n # igNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeIt # K/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1 # zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk # 8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsW # eupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAk # prxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0G # A1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQG # fHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYB # BQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEy # NTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hB # MjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB # MA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWL # pQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgj # g8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3Q # YIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5 # bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUG # tMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNE # suEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6U # Arb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG # 0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWV # FjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5 # t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjs # arfNZzGCBg4wggYKAgEBMDIwHjEcMBoGA1UEAwwTVkFEVEVLIENvZGUgU2lnbmlu # ZwIQEflOMRuxR6pMqkvTSLe5eTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3 # AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG # AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBoOnWTM9Mb # xvVTy67j0ndfrrNNxdbYt5qBY7AVjOAcbzANBgkqhkiG9w0BAQEFAASCAgBObvU5 # hwBD4GLWZjs3YE0vp1YhH7qjp8MK7FI15otHFYOn4HltnAyU44+BJO+9ZaFMzYYW # 5DELe1wSGbJID59Tm09N/oVK/8m3r3eOt/+iLMa/kTrusRmO8snfiwCX62wWDAZx # Z7YeBc4l04YCpDiSg8T5zjyDrJdTpJVKPjyW47h6pGcGXXGZi+QER3NVZLwfJGBE # ZiEKYQk1icivVgw8M8lN+MlgMiUkCFSVE7EtkFtHMbW9x8/wdrq9DAQQANvhdFYG # gPGn/VYHiyz6kMixb/65UfQaP1cS6fEJm5rTRP9fuG45LH8I8qLr+m4Uuhhz/4lo # cb79LXFqrJN36m6h27yPftu14o22/5Pxm6oPPfpuqYqSfm4/ZzQMonYHSabBfqzJ # t8gXAmGOvshSPoJ8E+iDbMsqPpJuyaQg6LnTEqpWyH7LQecgpc4M9vPP4DuUVi1c # nxG/jGd0Vj2B9y8DM11cm0xigZoEOrD3qZE+1NdYN8Ri4hGVvUnYwYy4H/XBg7H1 # p4AMKgUIrkA/f6chwq4jWUCL8hrcGtqtNEFZ0PdKonv40fzwktjsOTTjUj9uH4m2 # TnoKS3LvZE2Rqbm0pDmm58/jHdRuH4DtiYiRemizV2Erzr8vQcBqKVP+f0n4I0cB # 4Qt3pPsz7vJ0BvRTLJSh/LiRDUY2xG+QUVaS+KGCAyYwggMiBgkqhkiG9w0BCQYx # ggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg # UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeVdGgwDQYJYIZI # AWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ # BTEPFw0yNjA2MDEyMTIzNTJaMC8GCSqGSIb3DQEJBDEiBCDecWWAA3koklKH5Jdh # eh7dUfNE264tieWlIionXG1R3DANBgkqhkiG9w0BAQEFAASCAgCHft9f9JWiErmZ # K8jWcMyUDs6x/qVichhuABJ1RZB4w/w5h3y39LccLdQQJAhwubpF6CZ5mpvi8JVn # VvOc9M5yfqzRcaJTKfesKZBuf/uatelOl+bC1swjFax4gHbvleoBMsICVAqeJI9x # V0MkKH9C79tHGXYEuw1Sye/10NRNGvApnRtuJJTwF5ViPye7AmldJj+qXFjLjlHt # 81bZlQvlg2XsubX1rY+G8rJ+lhyEY03GffU/6AtilQbqftwD6+zAa8kTJCz86+gC # 30gq9f/GBxmLFQda/0K2UYxrhdKzwwG5GQAS2EEH1A5qd2veUBv+O4V5U5o+5yfx # dWC4dQYt/IAVcmkuzW++GSxG23t9GJUhKrAAgxKo2FjJeHsc+gTUfX+tpwM/cvMs # orjF4o1N0Q3dDrgf/SK2RV49Ttyv6jqwrVaptBRDCWL8N+VIPLYxn0wjf6eBUSNB # zRLzobErRwYzK/JAN5TaTbgtSjQNG1jOECWgwUa2mSbKQ85EQtggVr8pikaRO/Ym # pfkHd7l53EDftgdOq+C9kg7XtCvZu1mHfHzx4unc2WiIkCtqRDRY1VX5tV8492QF # EGXFTQoqUjSeoBxx2pitweLoyGyJZ2Hf0d519sPh1VM8b+xfbYCGvOwTO7zY05fM # cFDkwEN872T5wjm2un3Lidc8tYkn2w== # SIG # End signature block |