Public/Check-OwnerLensPrerequisites.ps1

<#
.SYNOPSIS
Checks whether the local machine is ready to run and collect data with OwnerLens.
 
.DESCRIPTION
Validates Windows/PowerShell prerequisites, module dependencies, local filesystem paths,
OwnerLens packaged/source runtime layout, Node.js availability, Microsoft Graph authentication,
Azure authentication, subscription access, and optional local runtime startup.
 
The command is read-only except for creating/removing tiny probe files in DataPath, LOCALAPPDATA,
and TEMP. If -TestRuntimeStartup is used, it starts OwnerLens on 127.0.0.1 with a temporary
runtime token and stops it after probing /api/data.
 
.EXAMPLE
Check-OwnerLensPrerequisites -SkipGraph -SkipAzure
 
Checks local system and runtime prerequisites without checking Microsoft Graph or Azure authentication.
 
.EXAMPLE
Check-OwnerLensPrerequisites -OutputJson -FailOnError
 
Writes the report as JSON and throws when one or more prerequisite checks fail.
#>


function Check-OwnerLensPrerequisites {
  [CmdletBinding()]
  param(
    [string]$RuntimePath = "",

    [string]$PackageRoot = "",

    [ValidateNotNullOrEmpty()]
    [string]$DataPath = (Join-Path (Get-Location) "data"),

    [string]$TenantId = "",

    [string]$SubscriptionIds = "",

    [ValidateRange(0, 65535)]
    [int]$Port = 0,

    [ValidateRange(0, 1024)]
    [int]$MinimumFreeDiskGB = 2,

    [switch]$SkipGraph,

    [switch]$SkipAzure,

    [switch]$SkipRuntime,

    [switch]$SkipOnlineChecks,

    [switch]$TestRuntimeStartup,

    [switch]$OutputJson,

    [switch]$FailOnError
  )

  Set-StrictMode -Version Latest
  $ErrorActionPreference = "Stop"

  $checks = [System.Collections.Generic.List[object]]::new()

function Add-OwnerLensCheck {
  param(
    [ValidateSet("Pass", "Warn", "Fail", "Info")]
    [string]$Status,

    [Parameter(Mandatory = $true)]
    [string]$Area,

    [Parameter(Mandatory = $true)]
    [string]$Name,

    [string]$Details = "",

    [string]$Fix = "",

    [object]$Data = $null
  )

  $checks.Add([pscustomobject]@{
    Status = $Status
    Area = $Area
    Name = $Name
    Details = $Details
    Fix = $Fix
    Data = $Data
  }) | Out-Null
}

function Test-OwnerLensCommand {
  param([Parameter(Mandatory = $true)][string]$Name)

  try {
    return Get-Command $Name -ErrorAction Stop
  } catch {
    return $null
  }
}

function Get-OwnerLensBestModule {
  param([Parameter(Mandatory = $true)][string]$Name)

  try {
    return Get-Module -ListAvailable -Name $Name |
      Sort-Object Version -Descending |
      Select-Object -First 1
  } catch {
    return $null
  }
}

function Test-OwnerLensModule {
  param(
    [Parameter(Mandatory = $true)][string]$Name,
    [Parameter(Mandatory = $true)][string]$Area,
    [string]$InstallHint = "",
    [switch]$Required
  )

  $module = Get-OwnerLensBestModule -Name $Name
  if (-not $module) {
    Add-OwnerLensCheck `
      -Status $(if ($Required) { "Fail" } else { "Warn" }) `
      -Area $Area `
      -Name "PowerShell module: $Name" `
      -Details "Module not found." `
      -Fix $(if ($InstallHint) { $InstallHint } else { "Install-Module $Name -Scope CurrentUser" })
    return $null
  }

  try {
    Import-Module $Name -ErrorAction Stop
    Add-OwnerLensCheck -Status "Pass" -Area $Area -Name "PowerShell module: $Name" -Details "Loaded $($module.Version) from $($module.ModuleBase)."
  } catch {
    Add-OwnerLensCheck `
      -Status $(if ($Required) { "Fail" } else { "Warn" }) `
      -Area $Area `
      -Name "PowerShell module import: $Name" `
      -Details $_.Exception.Message `
      -Fix $(if ($InstallHint) { $InstallHint } else { "Reinstall module: Install-Module $Name -Scope CurrentUser -Force" })
  }

  return $module
}

function Resolve-OwnerLensPath {
  param([string]$Path)

  if ([string]::IsNullOrWhiteSpace($Path)) {
    return ""
  }

  try {
    return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
  } catch {
    return [System.IO.Path]::GetFullPath($Path)
  }
}

function Test-OwnerLensWritableDirectory {
  param(
    [Parameter(Mandatory = $true)][string]$Area,
    [Parameter(Mandatory = $true)][string]$Name,
    [Parameter(Mandatory = $true)][string]$Path,
    [switch]$Create
  )

  try {
    $resolvedPath = Resolve-OwnerLensPath -Path $Path
    if (-not (Test-Path -LiteralPath $resolvedPath)) {
      if ($Create) {
        New-Item -ItemType Directory -Path $resolvedPath -Force | Out-Null
      } else {
        Add-OwnerLensCheck -Status "Fail" -Area $Area -Name $Name -Details "Directory does not exist: $resolvedPath" -Fix "Create the directory or pass a different path."
        return
      }
    }

    $probe = Join-Path $resolvedPath (".ownerlens-prereq-{0}.tmp" -f ([guid]::NewGuid().ToString("N")))
    "ownerlens" | Set-Content -LiteralPath $probe -Encoding UTF8
    Remove-Item -LiteralPath $probe -Force
    Add-OwnerLensCheck -Status "Pass" -Area $Area -Name $Name -Details "Writable: $resolvedPath"
  } catch {
    Add-OwnerLensCheck -Status "Fail" -Area $Area -Name $Name -Details $_.Exception.Message -Fix "Grant write access or use another path."
  }
}

function Test-OwnerLensDiskFree {
  param(
    [Parameter(Mandatory = $true)][string]$Path,
    [int]$MinimumGB
  )

  try {
    $resolvedPath = Resolve-OwnerLensPath -Path $Path
    if (-not (Test-Path -LiteralPath $resolvedPath)) {
      New-Item -ItemType Directory -Path $resolvedPath -Force | Out-Null
    }

    $root = [System.IO.Path]::GetPathRoot($resolvedPath)
    if (-not $root) {
      Add-OwnerLensCheck -Status "Warn" -Area "Storage" -Name "Free disk space" -Details "Could not determine drive root for $resolvedPath."
      return
    }

    $driveName = $root.TrimEnd("\")
    $drive = Get-PSDrive -Name $driveName.TrimEnd(":") -ErrorAction Stop
    $freeGB = [math]::Round(($drive.Free / 1GB), 2)
    $status = if ($freeGB -ge $MinimumGB) { "Pass" } else { "Warn" }
    Add-OwnerLensCheck -Status $status -Area "Storage" -Name "Free disk space" -Details "$freeGB GB free on $root. Minimum expected: $MinimumGB GB." -Fix $(if ($status -eq "Warn") { "Use a drive with more free space for large tenant snapshots." } else { "" })
  } catch {
    Add-OwnerLensCheck -Status "Warn" -Area "Storage" -Name "Free disk space" -Details $_.Exception.Message
  }
}

function Find-OwnerLensPackageRoot {
  param([string]$ExplicitPackageRoot)

  $candidates = [System.Collections.Generic.List[string]]::new()

  if (-not [string]::IsNullOrWhiteSpace($ExplicitPackageRoot)) {
    $candidates.Add((Resolve-OwnerLensPath -Path $ExplicitPackageRoot)) | Out-Null
  }

  $candidates.Add((Get-Location).ProviderPath) | Out-Null

  if ($PSScriptRoot) {
    $cursor = Resolve-OwnerLensPath -Path $PSScriptRoot
    for ($i = 0; $i -lt 6 -and -not [string]::IsNullOrWhiteSpace($cursor); $i++) {
      $candidates.Add($cursor) | Out-Null
      $parent = Split-Path -Parent $cursor
      if ($parent -eq $cursor) { break }
      $cursor = $parent
    }
  }

  foreach ($candidate in ($candidates | Select-Object -Unique)) {
    if (
      (Test-Path -LiteralPath (Join-Path $candidate "package.json")) -and
      (Test-Path -LiteralPath (Join-Path $candidate "bin\ownerlens.js"))
    ) {
      return $candidate
    }
  }

  return ""
}

function Find-OwnerLensModuleRoot {
  $candidates = [System.Collections.Generic.List[string]]::new()

  if ($PSScriptRoot) {
    $cursor = Resolve-OwnerLensPath -Path $PSScriptRoot
    for ($i = 0; $i -lt 6 -and -not [string]::IsNullOrWhiteSpace($cursor); $i++) {
      $candidates.Add($cursor) | Out-Null
      $candidates.Add((Join-Path $cursor "powershell\OwnerLens")) | Out-Null
      $parent = Split-Path -Parent $cursor
      if ($parent -eq $cursor) { break }
      $cursor = $parent
    }
  }

  $module = Get-OwnerLensBestModule -Name "OwnerLens"
  if ($module) {
    $candidates.Add($module.ModuleBase) | Out-Null
  }

  foreach ($candidate in ($candidates | Select-Object -Unique)) {
    if (
      (Test-Path -LiteralPath (Join-Path $candidate "OwnerLens.psd1")) -and
      (Test-Path -LiteralPath (Join-Path $candidate "OwnerLens.psm1"))
    ) {
      return $candidate
    }
  }

  return ""
}

function Resolve-OwnerLensRuntimeLayout {
  param(
    [string]$ExplicitRuntimePath,
    [string]$PackageRoot,
    [string]$ModuleRoot
  )

  $candidates = [System.Collections.Generic.List[string]]::new()

  if (-not [string]::IsNullOrWhiteSpace($ExplicitRuntimePath)) {
    $candidates.Add((Resolve-OwnerLensPath -Path $ExplicitRuntimePath)) | Out-Null
  }

  if (-not [string]::IsNullOrWhiteSpace($ModuleRoot)) {
    $candidates.Add((Join-Path $ModuleRoot "bin\win-x64")) | Out-Null
  }

  if ($env:LOCALAPPDATA) {
    $candidates.Add((Join-Path $env:LOCALAPPDATA "OwnerLens\runtime")) | Out-Null
  }

  if (-not [string]::IsNullOrWhiteSpace($PackageRoot)) {
    $candidates.Add($PackageRoot) | Out-Null
  }

  foreach ($candidate in ($candidates | Select-Object -Unique)) {
    if (-not (Test-Path -LiteralPath $candidate)) {
      continue
    }

    $packagedEntrypoint = Join-Path $candidate "app\bin\ownerlens.js"
    $sourceEntrypoint = Join-Path $candidate "bin\ownerlens.js"

    if (Test-Path -LiteralPath $packagedEntrypoint) {
      return [pscustomobject]@{
        Kind = "PackagedRuntime"
        RuntimeRoot = $candidate
        AppRoot = Join-Path $candidate "app"
        Entrypoint = $packagedEntrypoint
        NodePath = Join-Path $candidate "node.exe"
      }
    }

    if (Test-Path -LiteralPath $sourceEntrypoint) {
      return [pscustomobject]@{
        Kind = "SourcePackage"
        RuntimeRoot = $candidate
        AppRoot = $candidate
        Entrypoint = $sourceEntrypoint
        NodePath = ""
      }
    }
  }

  return $null
}

function Get-OwnerLensNodeVersion {
  param([Parameter(Mandatory = $true)][string]$NodePath)

  try {
    $output = & $NodePath --version 2>$null
    if ($LASTEXITCODE -ne 0) { return $null }
    return [string]$output
  } catch {
    return $null
  }
}

function ConvertTo-OwnerLensNodeVersionNumber {
  param([string]$Version)

  if ([string]::IsNullOrWhiteSpace($Version)) { return $null }
  $clean = $Version.Trim().TrimStart("v")
  try { return [version]$clean } catch { return $null }
}

function Test-OwnerLensRuntimeLayout {
  param([Parameter(Mandatory = $true)]$Layout)

  Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Runtime layout" -Details "$($Layout.Kind): $($Layout.RuntimeRoot)"

  $requiredPaths = @(
    @{ Name = "entrypoint"; Path = $Layout.Entrypoint },
    @{ Name = "package.json"; Path = (Join-Path $Layout.AppRoot "package.json") },
    @{ Name = "dist/index.html"; Path = (Join-Path $Layout.AppRoot "dist\index.html") },
    @{ Name = "dist-server/ownerlens-server.js"; Path = (Join-Path $Layout.AppRoot "dist-server\ownerlens-server.js") },
    @{ Name = "node_modules"; Path = (Join-Path $Layout.AppRoot "node_modules") },
    @{ Name = "migrations"; Path = (Join-Path $Layout.AppRoot "migrations") },
    @{ Name = "contracts"; Path = (Join-Path $Layout.AppRoot "contracts") }
  )

  foreach ($item in $requiredPaths) {
    if (Test-Path -LiteralPath $item.Path) {
      Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Runtime path: $($item.Name)" -Details $item.Path
    } else {
      Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime path: $($item.Name)" -Details "Missing: $($item.Path)" -Fix "Run npm run build and package the runtime, or run Install-OwnerLensRuntime -Force from a complete package."
    }
  }

  $packageJsonPath = Join-Path $Layout.AppRoot "package.json"
  if (Test-Path -LiteralPath $packageJsonPath) {
    try {
      $packageJson = Get-Content -LiteralPath $packageJsonPath -Raw | ConvertFrom-Json
      Add-OwnerLensCheck -Status "Info" -Area "Runtime" -Name "Package" -Details "$($packageJson.name) $($packageJson.version)"
    } catch {
      Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Package metadata" -Details "Could not parse package.json: $($_.Exception.Message)"
    }
  }

  $nodePath = ""
  if ($Layout.NodePath -and (Test-Path -LiteralPath $Layout.NodePath)) {
    $nodePath = $Layout.NodePath
    Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Bundled node.exe" -Details $nodePath
  } else {
    $nodeCommand = Test-OwnerLensCommand -Name "node.exe"
    if (-not $nodeCommand) {
      $nodeCommand = Test-OwnerLensCommand -Name "node"
    }

    if ($nodeCommand) {
      $nodePath = $nodeCommand.Source
      Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "System Node.js" -Details $nodePath
    } else {
      Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Node.js" -Details "No bundled node.exe and no node command in PATH." -Fix "Use a packaged Windows runtime with node.exe or install Node.js 20+."
    }
  }

  if ($nodePath) {
    $nodeVersionRaw = Get-OwnerLensNodeVersion -NodePath $nodePath
    $nodeVersion = ConvertTo-OwnerLensNodeVersionNumber -Version $nodeVersionRaw
    if (-not $nodeVersion) {
      Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Node.js version" -Details "Could not read Node.js version from $nodePath."
    } elseif ($nodeVersion.Major -lt 20) {
      Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Node.js version" -Details "$nodeVersionRaw. Expected Node.js 20+." -Fix "Upgrade Node.js or bundle a current node.exe."
    } else {
      Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Node.js version" -Details $nodeVersionRaw
    }
  }

  $duckDbApi = Join-Path $Layout.AppRoot "node_modules\@duckdb\node-api"
  $duckDbBindings = Join-Path $Layout.AppRoot "node_modules\@duckdb\node-bindings"
  if (Test-Path -LiteralPath $duckDbApi) {
    Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB node-api" -Details $duckDbApi
  } else {
    Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "DuckDB node-api" -Details "Missing @duckdb/node-api." -Fix "Run npm ci for source layout or rebuild packaged runtime with production dependencies."
  }

  if (Test-Path -LiteralPath $duckDbBindings) {
    Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB node-bindings" -Details $duckDbBindings
  } else {
    Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "DuckDB node-bindings" -Details "Missing @duckdb/node-bindings." -Fix "Run npm ci for source layout or rebuild packaged runtime with production dependencies."
  }

  if ($IsWindows) {
    $winBindingCandidates = @(
      (Join-Path $Layout.AppRoot "node_modules\@duckdb\node-bindings-win32-x64\duckdb.node")
      (Join-Path $Layout.AppRoot "node_modules\@duckdb\node-bindings-win32-arm64\duckdb.node")
    )
    $winBinding = $winBindingCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
    if ($winBinding) {
      Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB Windows native binding" -Details $winBinding
    } else {
      Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "DuckDB Windows native binding" -Details "Missing @duckdb Windows native binding package." -Fix "Rebuild/package on Windows x64, or ensure optional dependencies are included."
    }
  } else {
    $nativeBinding = Get-ChildItem -LiteralPath (Join-Path $Layout.AppRoot "node_modules\@duckdb") -Recurse -Filter "duckdb.node" -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($nativeBinding) {
      Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB native binding" -Details $nativeBinding.FullName
    } else {
      Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "DuckDB native binding" -Details "No duckdb.node found. This may fail at runtime."
    }
  }

  return $nodePath
}

function Get-OwnerLensFreePort {
  $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), 0)
  try {
    $listener.Start()
    return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
  } finally {
    $listener.Stop()
  }
}

function Test-OwnerLensPortAvailable {
  param([int]$PortToCheck)

  if ($PortToCheck -le 0) {
    try {
      $freePort = Get-OwnerLensFreePort
      Add-OwnerLensCheck -Status "Pass" -Area "Network" -Name "Loopback bind" -Details "Able to bind 127.0.0.1. Sample free port: $freePort."
    } catch {
      Add-OwnerLensCheck -Status "Fail" -Area "Network" -Name "Loopback bind" -Details $_.Exception.Message -Fix "Check local firewall/security software and loopback policy."
    }
    return
  }

  $listener = $null
  try {
    $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $PortToCheck)
    $listener.Start()
    Add-OwnerLensCheck -Status "Pass" -Area "Network" -Name "Port $PortToCheck" -Details "Available on 127.0.0.1."
  } catch {
    Add-OwnerLensCheck -Status "Fail" -Area "Network" -Name "Port $PortToCheck" -Details $_.Exception.Message -Fix "Stop the process using the port or run Start-OwnerLens -Port 0."
  } finally {
    if ($listener) { $listener.Stop() }
  }
}

function Test-OwnerLensRuntimeStartup {
  param(
    [Parameter(Mandatory = $true)]$Layout,
    [Parameter(Mandatory = $true)][string]$NodePath,
    [Parameter(Mandatory = $true)][string]$DataPath,
    [int]$RequestedPort
  )

  if (-not $NodePath) {
    Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime startup" -Details "Skipped because Node.js is not available."
    return
  }

  $portToUse = if ($RequestedPort -gt 0) { $RequestedPort } else { Get-OwnerLensFreePort }
  $token = [guid]::NewGuid().ToString("N")
  $stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) ("ownerlens-prereq-{0}.out.log" -f $token)
  $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) ("ownerlens-prereq-{0}.err.log" -f $token)
  $process = $null

  try {
    $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
    $startInfo.FileName = $NodePath
    $startInfo.WorkingDirectory = $Layout.AppRoot
    $startInfo.UseShellExecute = $false
    $startInfo.CreateNoWindow = $true
    $startInfo.RedirectStandardOutput = $true
    $startInfo.RedirectStandardError = $true
    $startInfo.ArgumentList.Add($Layout.Entrypoint)
    $startInfo.ArgumentList.Add("start")
    $startInfo.ArgumentList.Add("--host")
    $startInfo.ArgumentList.Add("127.0.0.1")
    $startInfo.ArgumentList.Add("--port")
    $startInfo.ArgumentList.Add([string]$portToUse)
    $startInfo.Environment["OWNERLENS_DATA_DIR"] = (Resolve-OwnerLensPath -Path $DataPath)
    $startInfo.Environment["OWNERLENS_RUNTIME_TOKEN"] = $token

    $process = [System.Diagnostics.Process]::Start($startInfo)
    if (-not $process) {
      throw "Process did not start."
    }

    $deadline = (Get-Date).AddSeconds(30)
    $url = "http://127.0.0.1:$portToUse/api/data"
    $lastError = ""

    while ((Get-Date) -lt $deadline) {
      if ($process.HasExited) {
        $stdout = $process.StandardOutput.ReadToEnd()
        $stderr = $process.StandardError.ReadToEnd()
        throw "Runtime exited with code $($process.ExitCode). stdout=$stdout stderr=$stderr"
      }

      try {
        $response = Invoke-RestMethod -Method GET -Uri $url -Headers @{ "x-ownerlens-runtime-token" = $token } -TimeoutSec 5 -ErrorAction Stop
        Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Runtime startup" -Details "Started and answered $url. Files=$(@($response.files).Count)."
        return
      } catch {
        $lastError = $_.Exception.Message
        Start-Sleep -Milliseconds 500
      }
    }

    throw "Runtime did not become ready within 30 seconds. Last error: $lastError"
  } catch {
    Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime startup" -Details $_.Exception.Message -Fix "Check runtime build, DuckDB native bindings, logs, and OWNERLENS_DATA_DIR permissions."
  } finally {
    if ($process -and -not $process.HasExited) {
      try { $process.Kill() } catch {}
      try { $process.WaitForExit(5000) | Out-Null } catch {}
    }
    Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
  }
}

function Test-OwnerLensSystem {
  Add-OwnerLensCheck -Status "Info" -Area "System" -Name "Machine" -Details "$env:COMPUTERNAME / $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription)"
  Add-OwnerLensCheck -Status "Info" -Area "System" -Name "Architecture" -Details ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString())

  if ($IsWindows) {
    Add-OwnerLensCheck -Status "Pass" -Area "System" -Name "Operating system" -Details "Windows detected."
  } else {
    Add-OwnerLensCheck -Status "Fail" -Area "System" -Name "Operating system" -Details "OwnerLens PowerShell module is currently Windows-only." -Fix "Run on Windows with PowerShell 7."
  }

  if ($PSVersionTable.PSEdition -eq "Core" -and $PSVersionTable.PSVersion.Major -ge 7) {
    Add-OwnerLensCheck -Status "Pass" -Area "System" -Name "PowerShell" -Details "$($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)"
  } else {
    Add-OwnerLensCheck -Status "Fail" -Area "System" -Name "PowerShell" -Details "$($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion). Expected PowerShell 7+." -Fix "Install PowerShell 7 and run with pwsh."
  }

  try {
    $policies = Get-ExecutionPolicy -List | ForEach-Object { "$($_.Scope)=$($_.ExecutionPolicy)" }
    $effective = Get-ExecutionPolicy
    $status = if ($effective -in @("Restricted", "AllSigned")) { "Warn" } else { "Pass" }
    Add-OwnerLensCheck -Status $status -Area "System" -Name "Execution policy" -Details ($policies -join "; ") -Fix $(if ($status -eq "Warn") { "Run packaged commands via pwsh -ExecutionPolicy Bypass, or adjust CurrentUser policy if allowed." } else { "" })
  } catch {
    Add-OwnerLensCheck -Status "Warn" -Area "System" -Name "Execution policy" -Details $_.Exception.Message
  }

  if ($env:LOCALAPPDATA) {
    Add-OwnerLensCheck -Status "Pass" -Area "System" -Name "LOCALAPPDATA" -Details $env:LOCALAPPDATA
  } else {
    Add-OwnerLensCheck -Status "Fail" -Area "System" -Name "LOCALAPPDATA" -Details "Environment variable is empty." -Fix "Run from a normal interactive Windows user profile."
  }

  if ($IsWindows) {
    try {
      $longPaths = Get-ItemPropertyValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -ErrorAction Stop
      $status = if ([int]$longPaths -eq 1) { "Pass" } else { "Warn" }
      Add-OwnerLensCheck -Status $status -Area "System" -Name "Long paths" -Details "LongPathsEnabled=$longPaths" -Fix $(if ($status -eq "Warn") { "Enable Windows long paths if npm/source layouts hit path length issues." } else { "" })
    } catch {
      Add-OwnerLensCheck -Status "Warn" -Area "System" -Name "Long paths" -Details "Could not read registry value: $($_.Exception.Message)"
    }
  }

  Test-OwnerLensWritableDirectory -Area "Storage" -Name "DataPath writable" -Path $DataPath -Create

  if ($env:LOCALAPPDATA) {
    Test-OwnerLensWritableDirectory -Area "Storage" -Name "OwnerLens app data writable" -Path (Join-Path $env:LOCALAPPDATA "OwnerLens") -Create
  }

  Test-OwnerLensWritableDirectory -Area "Storage" -Name "TEMP writable" -Path ([System.IO.Path]::GetTempPath())
  Test-OwnerLensDiskFree -Path $DataPath -MinimumGB $MinimumFreeDiskGB
  Test-OwnerLensPortAvailable -PortToCheck $Port

  foreach ($commandName in @("pwsh", "git", "npm")) {
    $command = Test-OwnerLensCommand -Name $commandName
    if ($command) {
      Add-OwnerLensCheck -Status "Info" -Area "System" -Name "Command: $commandName" -Details $command.Source
    } else {
      $status = if ($commandName -eq "pwsh") { "Fail" } else { "Warn" }
      Add-OwnerLensCheck -Status $status -Area "System" -Name "Command: $commandName" -Details "Not found in PATH."
    }
  }
}

function Test-OwnerLensGraph {
  if ($SkipGraph) {
    Add-OwnerLensCheck -Status "Info" -Area "Graph" -Name "Graph checks" -Details "Skipped by -SkipGraph."
    return
  }

  Test-OwnerLensModule -Name "Microsoft.Graph.Authentication" -Area "Graph" -Required -InstallHint "Install-Module Microsoft.Graph -Scope CurrentUser" | Out-Null
  Test-OwnerLensModule -Name "Microsoft.Graph.Applications" -Area "Graph" -Required -InstallHint "Install-Module Microsoft.Graph -Scope CurrentUser" | Out-Null
  Test-OwnerLensModule -Name "Microsoft.Graph.Groups" -Area "Graph" -InstallHint "Install-Module Microsoft.Graph -Scope CurrentUser" | Out-Null

  if (-not (Test-OwnerLensCommand -Name "Get-MgContext")) {
    Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph commands" -Details "Get-MgContext is not available." -Fix "Install/import Microsoft.Graph.Authentication."
    return
  }

  $context = $null
  try { $context = Get-MgContext } catch {}

  if (-not $context) {
    Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph connection" -Details "Not connected." -Fix "Connect-MgGraph -TenantId '<tenant-id>' -Scopes 'Application.Read.All','Group.Read.All','Directory.Read.All'"
    return
  }

  Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Graph connection" -Details "Tenant=$($context.TenantId); Account=$($context.Account); AuthType=$($context.AuthType)"

  if (-not [string]::IsNullOrWhiteSpace($TenantId) -and $context.TenantId -ne $TenantId) {
    Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Tenant match" -Details "Connected tenant is $($context.TenantId), expected $TenantId." -Fix "Reconnect: Disconnect-MgGraph; Connect-MgGraph -TenantId '$TenantId' -Scopes ..."
  } elseif (-not [string]::IsNullOrWhiteSpace($TenantId)) {
    Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Tenant match" -Details $TenantId
  }

  $requiredScopes = @("Application.Read.All", "Group.Read.All", "Directory.Read.All")
  $grantedScopes = @($context.Scopes)
  foreach ($scope in $requiredScopes) {
    if ($grantedScopes -contains $scope) {
      Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Graph scope: $scope" -Details "Granted."
    } else {
      Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph scope: $scope" -Details "Missing. Current scopes: $($grantedScopes -join ', ')" -Fix "Reconnect with scopes: $($requiredScopes -join ', ')"
    }
  }

  if ($SkipOnlineChecks) {
    Add-OwnerLensCheck -Status "Info" -Area "Graph" -Name "Graph API probes" -Details "Skipped by -SkipOnlineChecks."
    return
  }

  $graphProbes = @(
    @{ Name = "organization"; Uri = "/v1.0/organization?`$select=id,displayName" },
    @{ Name = "service principals"; Uri = "/v1.0/servicePrincipals?`$top=1&`$select=id,displayName,appId" },
    @{ Name = "applications"; Uri = "/v1.0/applications?`$top=1&`$select=id,displayName,appId" },
    @{ Name = "oauth2PermissionGrants"; Uri = "/v1.0/oauth2PermissionGrants?`$top=1&`$select=id,clientId,resourceId,scope" },
    @{ Name = "groups"; Uri = "/v1.0/groups?`$top=1&`$select=id,displayName" }
  )

  foreach ($probe in $graphProbes) {
    try {
      Invoke-MgGraphRequest -Method GET -Uri $probe.Uri -OutputType PSObject -ErrorAction Stop | Out-Null
      Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Graph probe: $($probe.Name)" -Details $probe.Uri
    } catch {
      Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph probe: $($probe.Name)" -Details $_.Exception.Message -Fix "Check Graph permissions/consent for OwnerLens collection."
    }
  }
}

function Test-OwnerLensAzure {
  if ($SkipAzure) {
    Add-OwnerLensCheck -Status "Info" -Area "Azure" -Name "Azure checks" -Details "Skipped by -SkipAzure."
    return
  }

  Test-OwnerLensModule -Name "Az.Accounts" -Area "Azure" -Required -InstallHint "Install-Module Az -Scope CurrentUser" | Out-Null
  Test-OwnerLensModule -Name "Az.Resources" -Area "Azure" -Required -InstallHint "Install-Module Az -Scope CurrentUser" | Out-Null
  Test-OwnerLensModule -Name "Az.ManagedServiceIdentity" -Area "Azure" -Required -InstallHint "Install-Module Az.ManagedServiceIdentity -Scope CurrentUser" | Out-Null

  $requiredCommands = @(
    "Get-AzContext",
    "Connect-AzAccount",
    "Get-AzSubscription",
    "Set-AzContext",
    "Invoke-AzRestMethod",
    "Get-AzResourceGroup",
    "Get-AzResource",
    "Get-AzRoleAssignment",
    "Get-AzUserAssignedIdentity"
  )

  foreach ($commandName in $requiredCommands) {
    $command = Test-OwnerLensCommand -Name $commandName
    if ($command) {
      Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Azure command: $commandName" -Details $command.Source
    } else {
      Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure command: $commandName" -Details "Missing." -Fix "Install/update Az modules."
    }
  }

  if (-not (Test-OwnerLensCommand -Name "Get-AzContext")) {
    return
  }

  $context = $null
  try { $context = Get-AzContext } catch {}

  if (-not $context) {
    Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure connection" -Details "Not connected." -Fix "Connect-AzAccount"
    return
  }

  Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Azure connection" -Details "Account=$($context.Account.Id); Tenant=$($context.Tenant.Id); Subscription=$($context.Subscription.Id) $($context.Subscription.Name)"

  if (-not [string]::IsNullOrWhiteSpace($TenantId) -and $context.Tenant.Id -ne $TenantId) {
    Add-OwnerLensCheck -Status "Warn" -Area "Azure" -Name "Azure tenant match" -Details "Azure context tenant is $($context.Tenant.Id), expected $TenantId." -Fix "Set-AzContext to the intended tenant/subscription."
  }

  $enabledSubscriptions = @()
  try {
    $enabledSubscriptions = @(Get-AzSubscription | Where-Object { $_.State -eq "Enabled" })
    Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Enabled subscriptions" -Details "Visible enabled subscriptions: $($enabledSubscriptions.Count)."
  } catch {
    Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Enabled subscriptions" -Details $_.Exception.Message -Fix "Check Azure login and tenant/subscription permissions."
    return
  }

  $subscriptionFilters = @()
  if ([string]::IsNullOrWhiteSpace($SubscriptionIds)) {
    if ($context.Subscription -and $context.Subscription.Id) {
      $subscriptionFilters = @([string]$context.Subscription.Id)
    }
  } else {
    $subscriptionFilters = @($SubscriptionIds.Split(",") | ForEach-Object { $_.Trim() } | Where-Object { $_ })
  }

  if ($subscriptionFilters.Count -eq 0) {
    Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Subscription selection" -Details "No subscription selected." -Fix "Set-AzContext -SubscriptionId '<id>' or pass -SubscriptionIds."
    return
  }

  $resolvedSubscriptions = @()
  foreach ($filter in $subscriptionFilters) {
    $sub = $enabledSubscriptions | Where-Object { $_.Id -eq $filter -or $_.Name -eq $filter } | Select-Object -First 1
    if (-not $sub) {
      Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Subscription: $filter" -Details "Not found or not enabled." -Fix "Use an enabled subscription ID/name visible to Get-AzSubscription."
      continue
    }

    $resolvedSubscriptions += $sub
    Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Subscription: $filter" -Details "$($sub.Name) ($($sub.Id))"
  }

  if ($SkipOnlineChecks) {
    Add-OwnerLensCheck -Status "Info" -Area "Azure" -Name "Azure API probes" -Details "Skipped by -SkipOnlineChecks."
    return
  }

  foreach ($sub in $resolvedSubscriptions) {
    try {
      Set-AzContext -SubscriptionId $sub.Id -ErrorAction Stop | Out-Null
      Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Set context: $($sub.Name)" -Details $sub.Id
    } catch {
      Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Set context: $($sub.Name)" -Details $_.Exception.Message
      continue
    }

    $encodedStart = [Uri]::EscapeDataString((Get-Date).AddMinutes(-15).ToUniversalTime().ToString("o"))
    $encodedEnd = [Uri]::EscapeDataString((Get-Date).ToUniversalTime().ToString("o"))
    $activityFilter = [Uri]::EscapeDataString("eventTimestamp ge '$([Uri]::UnescapeDataString($encodedStart))' and eventTimestamp le '$([Uri]::UnescapeDataString($encodedEnd))'")

    $azureProbes = @(
      @{ Name = "resource groups"; Path = "/subscriptions/$($sub.Id)/resourcegroups?api-version=2021-04-01" },
      @{ Name = "resources"; Path = "/subscriptions/$($sub.Id)/resources?api-version=2021-04-01" },
      @{ Name = "role assignments"; Path = "/subscriptions/$($sub.Id)/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$top=1" },
      @{ Name = "user-assigned managed identities"; Path = "/subscriptions/$($sub.Id)/providers/Microsoft.ManagedIdentity/userAssignedIdentities?api-version=2023-01-31" },
      @{ Name = "activity logs"; Path = "/subscriptions/$($sub.Id)/providers/microsoft.insights/eventtypes/management/values?api-version=2015-04-01&`$filter=$activityFilter" }
    )

    foreach ($probe in $azureProbes) {
      try {
        $response = Invoke-AzRestMethod -Method GET -Path $probe.Path -ErrorAction Stop
        $statusCode = [int]$response.StatusCode
        if ($statusCode -ge 200 -and $statusCode -lt 300) {
          Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Azure probe: $($probe.Name) / $($sub.Name)" -Details "HTTP $statusCode"
        } else {
          Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure probe: $($probe.Name) / $($sub.Name)" -Details "HTTP $statusCode $($response.Content)" -Fix "Check Azure RBAC permissions."
        }
      } catch {
        Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure probe: $($probe.Name) / $($sub.Name)" -Details $_.Exception.Message -Fix "Check Azure RBAC permissions and provider availability."
      }
    }
  }
}

function Test-OwnerLensExistingState {
  if (-not $env:LOCALAPPDATA) { return }

  $statePath = Join-Path $env:LOCALAPPDATA "OwnerLens\runtime-state.json"
  if (-not (Test-Path -LiteralPath $statePath)) {
    Add-OwnerLensCheck -Status "Info" -Area "Runtime" -Name "Existing runtime state" -Details "No state file found."
    return
  }

  try {
    $state = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json
    $process = $null
    if ($state.ProcessId) {
      $process = Get-Process -Id ([int]$state.ProcessId) -ErrorAction SilentlyContinue
    }

    if ($process) {
      Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Existing runtime state" -Details "State file points to running PID $($state.ProcessId) at $($state.ServerUrl)." -Fix "Run Stop-OwnerLens before starting a new pilot session if this is stale."
    } else {
      Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Stale runtime state" -Details "State file exists but process is not running: $statePath" -Fix "Run Stop-OwnerLens or delete the stale state file."
    }
  } catch {
    Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Existing runtime state" -Details "Could not parse $statePath`: $($_.Exception.Message)" -Fix "Delete the corrupted state file."
  }
}

function Write-OwnerLensReport {
  $summary = [pscustomobject]@{
    Pass = @($checks | Where-Object Status -eq "Pass").Count
    Warn = @($checks | Where-Object Status -eq "Warn").Count
    Fail = @($checks | Where-Object Status -eq "Fail").Count
    Info = @($checks | Where-Object Status -eq "Info").Count
  }

  if ($OutputJson) {
    [pscustomobject]@{
      summary = $summary
      checks = $checks
    } | ConvertTo-Json -Depth 8
    return
  }

  Write-Host ""
  Write-Host "OwnerLens prerequisite check"
  Write-Host "============================"
  Write-Host "Pass=$($summary.Pass) Warn=$($summary.Warn) Fail=$($summary.Fail) Info=$($summary.Info)"
  Write-Host ""

  $checks |
    Sort-Object @{ Expression = { switch ($_.Status) { "Fail" { 0 } "Warn" { 1 } "Pass" { 2 } default { 3 } } } }, Area, Name |
    Select-Object Status, Area, Name, Details, Fix |
    Format-Table -AutoSize -Wrap

  $failures = @($checks | Where-Object Status -eq "Fail")
  if ($failures.Count -gt 0) {
    Write-Host ""
    Write-Host "Blocking fixes:"
    foreach ($failure in $failures) {
      $fix = if ($failure.Fix) { $failure.Fix } else { "No automatic fix provided." }
      Write-Host "- [$($failure.Area)] $($failure.Name): $fix"
    }
  }
}

Test-OwnerLensSystem
Test-OwnerLensExistingState

$moduleRoot = Find-OwnerLensModuleRoot
if ($moduleRoot) {
  Add-OwnerLensCheck -Status "Pass" -Area "Module" -Name "OwnerLens module root" -Details $moduleRoot

  try {
    $manifest = Test-ModuleManifest -Path (Join-Path $moduleRoot "OwnerLens.psd1") -ErrorAction Stop
    Add-OwnerLensCheck -Status "Pass" -Area "Module" -Name "OwnerLens manifest" -Details "Version=$($manifest.Version); PowerShellVersion=$($manifest.PowerShellVersion)"
  } catch {
    Add-OwnerLensCheck -Status "Fail" -Area "Module" -Name "OwnerLens manifest" -Details $_.Exception.Message
  }
} else {
  Add-OwnerLensCheck -Status "Warn" -Area "Module" -Name "OwnerLens module root" -Details "Could not find OwnerLens.psd1/OwnerLens.psm1." -Fix "Run from the repository/package root or install/import the OwnerLens module."
}

$resolvedPackageRoot = Find-OwnerLensPackageRoot -ExplicitPackageRoot $PackageRoot
if ($resolvedPackageRoot) {
  Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "OwnerLens package root" -Details $resolvedPackageRoot
} else {
  Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "OwnerLens package root" -Details "Could not find package.json + bin/ownerlens.js. Packaged runtime may still be valid."
}

$runtimeLayout = $null
$nodePath = ""
if ($SkipRuntime) {
  Add-OwnerLensCheck -Status "Info" -Area "Runtime" -Name "Runtime checks" -Details "Skipped by -SkipRuntime."
} else {
  $runtimeLayout = Resolve-OwnerLensRuntimeLayout -ExplicitRuntimePath $RuntimePath -PackageRoot $resolvedPackageRoot -ModuleRoot $moduleRoot
  if (-not $runtimeLayout) {
    Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime layout" -Details "Could not find packaged runtime or source package." -Fix "Run Install-OwnerLensRuntime -Force, pass -RuntimePath, or run from a built OwnerLens repository/package root."
  } else {
    $nodePath = Test-OwnerLensRuntimeLayout -Layout $runtimeLayout
    if ($TestRuntimeStartup) {
      Test-OwnerLensRuntimeStartup -Layout $runtimeLayout -NodePath $nodePath -DataPath $DataPath -RequestedPort $Port
    }
  }
}

Test-OwnerLensGraph
Test-OwnerLensAzure
Write-OwnerLensReport

if ($FailOnError -and (@($checks | Where-Object Status -eq "Fail").Count -gt 0)) {
  throw "OwnerLens prerequisite checks failed."
}
}

# SIG # Begin signature block
# MII9MQYJKoZIhvcNAQcCoII9IjCCPR4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDMgu/GIWb6l3Kg
# RdIV7Xvd/70/mJupe8IGjfLmhCxolaCCIfYwggXMMIIDtKADAgECAhBUmNLR1FsZ
# lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu
# dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy
# MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv
# c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo
# b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD
# Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH
# or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD
# PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV
# qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70
# ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1
# vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3
# W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK
# xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg
# trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj
# O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7
# swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE
# AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ
# T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn
# LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c
# Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF
# xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx
# AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8
# FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY
# lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2
# 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY
# uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al
# GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk
# ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T
# 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggaoMIIEkKADAgECAhMzAAKajpPw
# gw8GGQINAAAAApqOMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ
# RCBWZXJpZmllZCBDUyBBT0MgQ0EgMDMwHhcNMjYwNjI5MTIyNTE0WhcNMjYwNzAy
# MTIyNTE0WjBpMQswCQYDVQQGEwJQTDEXMBUGA1UECB4OAUEA8wBkAHoAawBpAGUx
# DzANBgNVBAcTBldpZWx1bjEXMBUGA1UEChMOc29mdDRwbSBzcCB6b28xFzAVBgNV
# BAMTDnNvZnQ0cG0gc3Agem9vMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKC
# AYEAhMGKmy7Gt4CE101CTF4dlSRRUeC0YxhbrNfAcmOBSzaCL03tsI23+QAHEo4P
# VjpT57aSNoOUYkiGV9UfgIvs/GtJwwjUf45yu10umzM++kISP8lU1vWXcStG0QEk
# gvEnJERags1RU5Qxoc8BOWoKTU7YZ53sFOmjJ2w57+y0wR3HCddL5ZXClVhXifks
# P9hvOtgjaNl1gNavDm4dU/1IImMM/0V++5ZaIbeg/XzATbexJ873gUZsZzXKifra
# UnS0hhE6w79w/ZWlK9OAOa5X1wEY7CiaFCqlaVomkQ2QxxF8EaakwQAQPNf4jgAw
# zGwGB1efKF05XSHxXp8t8eTAI0EphTv2rsgbViHM7g6s7P6g3JlN0tec9AndUwyk
# e3iFP3yQPNTgkhIOumKVWR9BtOSU3r99sCMdxejvvA4+LMWngflM2g7tCH3iP4UK
# 1GPerM/x4q9aAzPddlODjZD1JlO6gMUbZr/h/ufq7mX7TYC03tRbbdpm8wCM/OZ4
# pfMvAgMBAAGjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA9
# BgNVHSUENjA0BgorBgEEAYI3YQEABggrBgEFBQcDAwYcKwYBBAGCN2GCycz3dYGA
# 0Y0Tg469piSB0sq0aDAdBgNVHQ4EFgQU7TqVKg6lwu/5YtvRThb6akqaJ1QwHwYD
# VR0jBBgwFoAUpEMMf3ZapYXnPo0oDwwXokVpcMYwZwYDVR0fBGAwXjBcoFqgWIZW
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIw
# SUQlMjBWZXJpZmllZCUyMENTJTIwQU9DJTIwQ0ElMjAwMy5jcmwwdAYIKwYBBQUH
# AQEEaDBmMGQGCCsGAQUFBzAChlhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# b3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDUyUyMEFPQyUy
# MENBJTIwMDMuY3J0MFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNo
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5o
# dG0wDQYJKoZIhvcNAQEMBQADggIBAAmUdPVUR0c2s5+vKT3aTWsxLxt2Mc5UgcjT
# LrNneWS/25jguHMrkQvgP4ZYxBe9lpMzp0GKu/kT6DE6BhrruioLsRQ0nwKkSt+v
# PasSxuX/KBPxzzb7lcOY8eUoFvs693mnkOMwSw6yxUi4bKvamQ5TgXq96Onz4It6
# bIQwKWFYedSPfD0ZmTsn4PWllcxZLjsNbiVd9prz37R537drRiIp3CfMBaqAx6gg
# 6DelPiXxLZMP+n8b2TL2foZNxdVKdRm0Y90uu2HJ3UgCV2BAhkIFk1BxUj9xjvcR
# eOUgd9r89LcQ4bhXzLOI3bGFWzGH/ueI+MV9J85zu7I8ecT83bvbGOs7wctQh/sc
# AK8XvoXWnq/6RVv+rgXk0RgwysQQEHnO0z0+2lQK4UeJpsQ2WtmmxEgS05aEEIx6
# ID8MUkp72rY4mN9weHFzKfikjuX06Hjv9YzTFovHFD3Tdtmiy7m/etv3F+rgDbWa
# 1cf0E4rmDlRIvqklxA8hpbum6uZJxsZnTfK30fZtPbE4D6MpQjaJdp2xm9fB0hEs
# NcijvF+9wUjM1gvS8InRiMmo2Kvk+4Xhu+cjYqjyW5i4Wew6N20oRBpycwbLvJT4
# qXgFp/RCBeTe+/llgmsL5gf38f1fiqEd4jYGJb1yJIX3zjDv+6zCwHGLVN1e33gR
# yV1GaGG5MIIGqDCCBJCgAwIBAgITMwACmo6T8IMPBhkCDQAAAAKajjANBgkqhkiG
# 9w0BAQwFADBaMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSswKQYDVQQDEyJNaWNyb3NvZnQgSUQgVmVyaWZpZWQgQ1MgQU9DIENB
# IDAzMB4XDTI2MDYyOTEyMjUxNFoXDTI2MDcwMjEyMjUxNFowaTELMAkGA1UEBhMC
# UEwxFzAVBgNVBAgeDgFBAPMAZAB6AGsAaQBlMQ8wDQYDVQQHEwZXaWVsdW4xFzAV
# BgNVBAoTDnNvZnQ0cG0gc3Agem9vMRcwFQYDVQQDEw5zb2Z0NHBtIHNwIHpvbzCC
# AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAITBipsuxreAhNdNQkxeHZUk
# UVHgtGMYW6zXwHJjgUs2gi9N7bCNt/kABxKOD1Y6U+e2kjaDlGJIhlfVH4CL7Pxr
# ScMI1H+OcrtdLpszPvpCEj/JVNb1l3ErRtEBJILxJyREWoLNUVOUMaHPATlqCk1O
# 2Ged7BTpoydsOe/stMEdxwnXS+WVwpVYV4n5LD/YbzrYI2jZdYDWrw5uHVP9SCJj
# DP9FfvuWWiG3oP18wE23sSfO94FGbGc1yon62lJ0tIYROsO/cP2VpSvTgDmuV9cB
# GOwomhQqpWlaJpENkMcRfBGmpMEAEDzX+I4AMMxsBgdXnyhdOV0h8V6fLfHkwCNB
# KYU79q7IG1YhzO4OrOz+oNyZTdLXnPQJ3VMMpHt4hT98kDzU4JISDrpilVkfQbTk
# lN6/fbAjHcXo77wOPizFp4H5TNoO7Qh94j+FCtRj3qzP8eKvWgMz3XZTg42Q9SZT
# uoDFG2a/4f7n6u5l+02AtN7UW23aZvMAjPzmeKXzLwIDAQABo4IB1jCCAdIwDAYD
# VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwPQYDVR0lBDYwNAYKKwYBBAGCN2EB
# AAYIKwYBBQUHAwMGHCsGAQQBgjdhgsnM93WBgNGNE4OOvaYkgdLKtGgwHQYDVR0O
# BBYEFO06lSoOpcLv+WLb0U4W+mpKmidUMB8GA1UdIwQYMBaAFKRDDH92WqWF5z6N
# KA8MF6JFaXDGMGcGA1UdHwRgMF4wXKBaoFiGVmh0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDUyUy
# MEFPQyUyMENBJTIwMDMuY3JsMHQGCCsGAQUFBwEBBGgwZjBkBggrBgEFBQcwAoZY
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQl
# MjBJRCUyMFZlcmlmaWVkJTIwQ1MlMjBBT0MlMjBDQSUyMDAzLmNydDBUBgNVHSAE
# TTBLMEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMA0GCSqGSIb3DQEBDAUAA4IC
# AQAJlHT1VEdHNrOfryk92k1rMS8bdjHOVIHI0y6zZ3lkv9uY4LhzK5EL4D+GWMQX
# vZaTM6dBirv5E+gxOgYa67oqC7EUNJ8CpErfrz2rEsbl/ygT8c82+5XDmPHlKBb7
# Ovd5p5DjMEsOssVIuGyr2pkOU4F6vejp8+CLemyEMClhWHnUj3w9GZk7J+D1pZXM
# WS47DW4lXfaa89+0ed+3a0YiKdwnzAWqgMeoIOg3pT4l8S2TD/p/G9ky9n6GTcXV
# SnUZtGPdLrthyd1IAldgQIZCBZNQcVI/cY73EXjlIHfa/PS3EOG4V8yziN2xhVsx
# h/7niPjFfSfOc7uyPHnE/N272xjrO8HLUIf7HACvF76F1p6v+kVb/q4F5NEYMMrE
# EBB5ztM9PtpUCuFHiabENlrZpsRIEtOWhBCMeiA/DFJKe9q2OJjfcHhxcyn4pI7l
# 9Oh47/WM0xaLxxQ903bZosu5v3rb9xfq4A21mtXH9BOK5g5USL6pJcQPIaW7purm
# ScbGZ03yt9H2bT2xOA+jKUI2iXadsZvXwdIRLDXIo7xfvcFIzNYL0vCJ0YjJqNir
# 5PuF4bvnI2Ko8luYuFnsOjdtKEQacnMGy7yU+Kl4Baf0QgXk3vv5ZYJrC+YH9/H9
# X4qhHeI2BiW9ciSF984w7/uswsBxi1TdXt94EcldRmhhuTCCBygwggUQoAMCAQIC
# EzMAAAAYDeuRVamKAJgAAAAAABgwDQYJKoZIhvcNAQEMBQAwYzELMAkGA1UEBhMC
# VVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE0MDIGA1UEAxMrTWlj
# cm9zb2Z0IElEIFZlcmlmaWVkIENvZGUgU2lnbmluZyBQQ0EgMjAyMTAeFw0yNjAz
# MjYxODExMzJaFw0zMTAzMjYxODExMzJaMFoxCzAJBgNVBAYTAlVTMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJRCBW
# ZXJpZmllZCBDUyBBT0MgQ0EgMDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
# AoICAQDIgNpgNFaiif2VWeWP5I6PnFXxJ/lB37fJR55GCvR7GLZBMkBijbiKVwgp
# BI3xM5nf484znH/qncJ+OCq6y3jgnQW+R8Zd7U+7LjlrmcskalzSQ0ghMxEpnBW8
# /HHs2V8ZJzQk6HP+SDsbvsL7LdlH/eO2l4mknhDBwr0Z/Q966TvEth5b8kCxj1vq
# iV4YNthLGRqZR9u2fK/yBMWu83p6O4uo2Edg++gEew5IL7vnnnKFqmSh/R9vPJy3
# WF1YcZewAUx8sXZNUnx3ZhVg59l2LpitPiwzE6FMqIsqaEvVe3MzuFd2a/uWDZH6
# VbDyUiRK78mIg1DQYA9zDEyyBFcNI+nxVSzglvL6u7PRuNqgcV3sf6ELxw89ysQM
# /Z4R1hRFWXRpyOWKKAKtfBHTk0UnNiPcxmLMMYs8jeUjOidfVPjTIry/UVwnwxdl
# kK85cZfBEMYZ/DBNOwdomP459Y1n8izKkbhsa+p4lw+cQVxATBFx9ggR79HhryT7
# HDmpPLvkJvBZ4wW4CW32UT2SMyDe28nIOU3m+hfHlVeKcLBQcym5VoRDjIcCVI7u
# qgGW2PNME0cfei8zCwCy6HCsssJWFS7eg/YbFhnATJcyWfMrkNuAbMfMN8Npg8cr
# S6jVVowyD0GG5zdgi+uQVcSK/638mA1xEYK3pnIoQgO09uuDBwIDAQABo4IB3DCC
# AdgwDgYDVR0PAQH/BAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBSk
# Qwx/dlqlhec+jSgPDBeiRWlwxjBUBgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9z
# aXRvcnkuaHRtMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMBIGA1UdEwEB/wQI
# MAYBAf8CAQAwHwYDVR0jBBgwFoAU2UEpsA8PY2zvadf1zSmepEhqMOYwcAYDVR0f
# BGkwZzBloGOgYYZfaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwv
# TWljcm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENvZGUlMjBTaWduaW5nJTIwUENB
# JTIwMjAyMS5jcmwwfQYIKwYBBQUHAQEEcTBvMG0GCCsGAQUFBzAChmFodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIw
# VmVyaWZpZWQlMjBDb2RlJTIwU2lnbmluZyUyMFBDQSUyMDIwMjEuY3J0MA0GCSqG
# SIb3DQEBDAUAA4ICAQBxxyBW+X6mhdRiSwD9PMMWcGUAnx5/QUwnNvZdFGEX+4DR
# DIr9WCh4C87wHtw+lg1D3uzK10DstPX0LFLBFAC3vWMYX4ImXwoLhoR0xlN8mUdo
# rJ3bgnpCJWuI1531Z1rCwPuUrSkBxfOIGDk3p2ECb3Ho/xHi5PRSR/OUrWuQHwXi
# aXMTuXu3IRLezwVkZpFmNwYRD57R9Nx2F/yM7tzOY0Hh0hGCaYEK38/6FrS0SXad
# XWyDUCfn5XOGACRjUCnHx+JQUG0f4SHD+iblpAI0gl+ZHnVmdXXxHTZeTa0CYCIh
# FxKP2922s0g6zLmeiV13LWUmtt/UF7TrWXpMi2/0UNniaDoH7rnPGRV5xVX8uXy4
# sZii4aswzqPM7Y7+mzcranqZ8EjZk5gjLhQ3A2sZaprlOu8CaRmyfcIiVH7zVfgA
# vm81MWXFziAf7my7QOvnyEFPGddq8MSfPtfRyw/Uq3uH6KpoaJNIfPYH6fceZSi5
# 3Rat1A9grExq3ROjhhSpTcchuBItAMNVPxoKNbUm+iR/X3XkL+9WQginjyHe+hXL
# clY8vAGXFD1p40PqMIpAYsmEJBFKW9df4//1N5oQDr/FY9IBJl/oSS979i5rtT7N
# Zz9KvYraCPRBGs0QCy+sWvgQa0coM70QJVLeVwmSxUO/0od0w9Qry7bSLrxGoDCC
# B54wggWGoAMCAQICEzMAAAAHh6M0o3uljhwAAAAAAAcwDQYJKoZIhvcNAQEMBQAw
# dzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjFI
# MEYGA1UEAxM/TWljcm9zb2Z0IElkZW50aXR5IFZlcmlmaWNhdGlvbiBSb290IENl
# cnRpZmljYXRlIEF1dGhvcml0eSAyMDIwMB4XDTIxMDQwMTIwMDUyMFoXDTM2MDQw
# MTIwMTUyMFowYzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjE0MDIGA1UEAxMrTWljcm9zb2Z0IElEIFZlcmlmaWVkIENvZGUgU2ln
# bmluZyBQQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALLw
# wK8ZiCji3VR6TElsaQhVCbRS/3pK+MHrJSj3Zxd3KU3rlfL3qrZilYKJNqztA9OQ
# acr1AwoNcHbKBLbsQAhBnIB34zxf52bDpIO3NJlfIaTE/xrweLoQ71lzCHkD7A4A
# s1Bs076Iu+mA6cQzsYYH/Cbl1icwQ6C65rU4V9NQhNUwgrx9rGQ//h890Q8JdjLL
# w0nV+ayQ2Fbkd242o9kH82RZsH3HEyqjAB5a8+Ae2nPIPc8sZU6ZE7iRrRZywRmr
# KDp5+TcmJX9MRff241UaOBs4NmHOyke8oU1TYrkxh+YeHgfWo5tTgkoSMoayqoDp
# HOLJs+qG8Tvh8SnifW2Jj3+ii11TS8/FGngEaNAWrbyfNrC69oKpRQXY9bGH6jn9
# NEJv9weFxhTwyvx9OJLXmRGbAUXN1U9nf4lXezky6Uh/cgjkVd6CGUAf0K+Jw+GE
# /5VpIVbcNr9rNE50Sbmy/4RTCEGvOq3GhjITbCa4crCzTTHgYYjHs1NbOc6brH+e
# KpWLtr+bGecy9CrwQyx7S/BfYJ+ozst7+yZtG2wR461uckFu0t+gCwLdN0A6cFtS
# RtR8bvxVFyWwTtgMMFRuBa3vmUOTnfKLsLefRaQcVTgRnzeLzdpt32cdYKp+dhr2
# ogc+qM6K4CBI5/j4VFyC4QFeUP2YAidLtvpXRRo3AgMBAAGjggI1MIICMTAOBgNV
# HQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNlBKbAPD2Ns
# 72nX9c0pnqRIajDmMFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNo
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5o
# dG0wGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUwAwEB/zAf
# BgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0wezB5oHeg
# dYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlmaWNhdGUl
# MjBBdXRob3JpdHklMjAyMDIwLmNybDCBwwYIKwYBBQUHAQEEgbYwgbMwgYEGCCsG
# AQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p
# Y3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRp
# ZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwLQYIKwYBBQUHMAGGIWh0dHA6
# Ly9vbmVvY3NwLm1pY3Jvc29mdC5jb20vb2NzcDANBgkqhkiG9w0BAQwFAAOCAgEA
# fyUqnv7Uq+rdZgrbVyNMul5skONbhls5fccPlmIbzi+OwVdPQ4H55v7VOInnmezQ
# EeW4LqK0wja+fBznANbXLB0KrdMCbHQpbLvG6UA/Xv2pfpVIE1CRFfNF4XKO8XYE
# a3oW8oVH+KZHgIQRIwAbyFKQ9iyj4aOWeAzwk+f9E5StNp5T8FG7/VEURIVWArbA
# zPt9ThVN3w1fAZkF7+YU9kbq1bCR2YD+MtunSQ1Rft6XG7b4e0ejRA7mB2IoX5hN
# h3UEauY0byxNRG+fT2MCEhQl9g2i2fs6VOG19CNep7SquKaBjhWmirYyANb0RJSL
# WjinMLXNOAga10n8i9jqeprzSMU5ODmrMCJE12xS/NWShg/tuLjAsKP6SzYZ+1Ry
# 358ZTFcx0FS/mx2vSoU8s8HRvy+rnXqyUJ9HBqS0DErVLjQwK8VtsBdekBmdTbQV
# oCgPCqr+PDPB3xajYnzevs7eidBsM71PINK2BoE2UfMwxCCX3mccFgx6UsQeRSdV
# VVNSyALQe6PT12418xon2iDGE81OGCreLzDcMAZnrUAx4XQLUz6ZTl65yPUiOh3k
# 7Yww94lDf+8oG2oZmDh5O1Qe38E+M3vhKwmzIeoB1dVLlz4i3IpaDcR+iuGjH2Td
# aC1ZOmBXiCRKJLj4DT2uhJ04ji+tHD6n58vhavFIrmcxghqRMIIajQIBATBxMFox
# CzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzAp
# BgNVBAMTIk1pY3Jvc29mdCBJRCBWZXJpZmllZCBDUyBBT0MgQ0EgMDMCEzMAApqO
# k/CDDwYZAg0AAAACmo4wDQYJYIZIAWUDBAIBBQCgXjAQBgorBgEEAYI3AgEMMQIw
# ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAvBgkqhkiG9w0BCQQxIgQgjbUa
# MzxbO+qGh3VYQf2ubmqk+sb+/mVtiyFta1wGJKowDQYJKoZIhvcNAQEBBQAEggGA
# CSyAYYHDgKWAbwRqBOxMalX+bS4edUi4CsE3nxF8jI4vQTwLn71BXk/MvFPfirWG
# bAtk/7W4KtFJLy5JQMiEFLZYkj6ZawtnZR57o+S5RrFrVtpWsjWAPUCNsoiMzFhI
# Pwee0VmWZvmnjn1oH61Uq4vo+XR7ZsG4Dvvjn+5blfo8CEnHOwOkjGne/3fMql1T
# MBoBqv3jTNVLdq7czGG6nr5053GEd0zstC8NJHk8ZpFv9Rpqz0eOM0K6r9+clara
# BkpVXBsU+4ThJDFcPKvQ4Hnjc267nyCqBLc5Dwv9NLZ8y0yC/+7Gm234Q8MJELnZ
# SR48YROsAxfodR7ylxFkrhZVlC2FzCmW3/XIOYmpuTWN1XBTaU9bQ8PnYzvUdsLk
# 3wSN5JxhKKW3QBDFFzAUlM/rmtvRAYoxhIgNCblLMzV+3vtd4iHANvOfa8VvPCAR
# Q8A/5KtzB3zgFj1sTigGJ9etUpZ46TFEDJi2FAbgIlWEjBA9/THtpIkhe+8W9abH
# oYIYETCCGA0GCisGAQQBgjcDAwExghf9MIIX+QYJKoZIhvcNAQcCoIIX6jCCF+YC
# AQMxDzANBglghkgBZQMEAgEFADCCAWIGCyqGSIb3DQEJEAEEoIIBUQSCAU0wggFJ
# AgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIL8tzSsQ+w8Y8rqFHtDD
# 9k2QzfBYuOhAj7PKFHzQuHytAgZp6IGKLVUYEzIwMjYwNjMwMjA0ODEwLjc4M1ow
# BIACAfSggeGkgd4wgdsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNV
# BAsTHm5TaGllbGQgVFNTIEVTTjo3RDAwLTA1RTAtRDk0NzE1MDMGA1UEAxMsTWlj
# cm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBTdGFtcGluZyBBdXRob3JpdHmggg8hMIIH
# gjCCBWqgAwIBAgITMwAAAAXlzw//Zi7JhwAAAAAABTANBgkqhkiG9w0BAQwFADB3
# MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMUgw
# RgYDVQQDEz9NaWNyb3NvZnQgSWRlbnRpdHkgVmVyaWZpY2F0aW9uIFJvb3QgQ2Vy
# dGlmaWNhdGUgQXV0aG9yaXR5IDIwMjAwHhcNMjAxMTE5MjAzMjMxWhcNMzUxMTE5
# MjA0MjMxWjBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBp
# bmcgQ0EgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ5851Jj
# /eDFnwV9Y7UGIqMcHtfnlzPREwW9ZUZHd5HBXXBvf7KrQ5cMSqFSHGqg2/qJhYqO
# QxwuEQXG8kB41wsDJP5d0zmLYKAY8Zxv3lYkuLDsfMuIEqvGYOPURAH+Ybl4SJEE
# Snt0MbPEoKdNihwM5xGv0rGofJ1qOYSTNcc55EbBT7uq3wx3mXhtVmtcCEr5ZKTk
# KKE1CxZvNPWdGWJUPC6e4uRfWHIhZcgCsJ+sozf5EeH5KrlFnxpjKKTavwfFP6Xa
# GZGWUG8TZaiTogRoAlqcevbiqioUz1Yt4FRK53P6ovnUfANjIgM9JDdJ4e0qiDRm
# 5sOTiEQtBLGd9Vhd1MadxoGcHrRCsS5rO9yhv2fjJHrmlQ0EIXmp4DhDBieKUGR+
# eZ4CNE3ctW4uvSDQVeSp9h1SaPV8UWEfyTxgGjOsRpeexIveR1MPTVf7gt8hY64X
# NPO6iyUGsEgt8c2PxF87E+CO7A28TpjNq5eLiiunhKbq0XbjkNoU5JhtYUrlmAbp
# xRjb9tSreDdtACpm3rkpxp7AQndnI0Shu/fk1/rE3oWsDqMX3jjv40e8KN5YsJBn
# czyWB4JyeeFMW3JBfdeAKhzohFe8U5w9WuvcP1E8cIxLoKSDzCCBOu0hWdjzKNu8
# Y5SwB1lt5dQhABYyzR3dxEO/T1K/BVF3rV69AgMBAAGjggIbMIICFzAOBgNVHQ8B
# Af8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGtpKDo1L0hjQM97
# 2K9J6T7ZPdshMFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNodHRw
# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0w
# EwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEw
# DwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2io
# ojCBhAYDVR0fBH0wezB5oHegdYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br
# aW9wcy9jcmwvTWljcm9zb2Z0JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBS
# b290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDIwLmNybDCBlAYIKwYB
# BQUHAQEEgYcwgYQwgYEGCCsGAQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0
# aW9uJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQw
# DQYJKoZIhvcNAQEMBQADggIBAF+Idsd+bbVaFXXnTHho+k7h2ESZJRWluLE0Oa/p
# O+4ge/XEizXvhs0Y7+KVYyb4nHlugBesnFqBGEdC2IWmtKMyS1OWIviwpnK3aL5J
# edwzbeBF7POyg6IGG/XhhJ3UqWeWTO+Czb1c2NP5zyEh89F72u9UIw+IfvM9lzDm
# c2O2END7MPnrcjWdQnrLn1Ntday7JSyrDvBdmgbNnCKNZPmhzoa8PccOiQljjTW6
# GePe5sGFuRHzdFt8y+bN2neF7Zu8hTO1I64XNGqst8S+w+RUdie8fXC1jKu3m9KG
# IqF4aldrYBamyh3g4nJPj/LR2CBaLyD+2BuGZCVmoNR/dSpRCxlot0i79dKOChmo
# ONqbMI8m04uLaEHAv4qwKHQ1vBzbV/nG89LDKbRSSvijmwJwxRxLLpMQ/u4xXxFf
# R4f/gksSkbJp7oqLwliDm/h+w0aJ/U5ccnYhYb7vPKNMN+SZDWycU5ODIRfyoGl5
# 9BsXR/HpRGtiJquOYGmvA/pk5vC1lcnbeMrcWD/26ozePQ/TWfNXKBOmkFpvPE8C
# H+EeGGWzqTCjdAsno2jzTeNSxlx3glDGJgcdz5D/AAxw9Sdgq/+rY7jjgs7X6fqP
# TXPmaCAJKVHAP19oEjJIBwD1LyHbaEgBxFCogYSOiUIr0Xqcr1nJfiWG2GwYe6Zo
# AF1bMIIHlzCCBX+gAwIBAgITMwAAAFXZ3WkmKPn44gAAAAAAVTANBgkqhkiG9w0B
# AQwFADBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcg
# Q0EgMjAyMDAeFw0yNTEwMjMyMDQ2NDlaFw0yNjEwMjIyMDQ2NDlaMIHbMQswCQYD
# VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3Nv
# ZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046
# N0QwMC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRp
# bWUgU3RhbXBpbmcgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAvbkfkh5ZSLP0MCUWafaw/KZoVZu9iQx8r5JwhZvdrUi86UjCCFQONjQa
# nrIxGF9hRGIZLQZ50gHrLC+4fpUEJff5t04VwByWC2/bWOuk6NmaTh9JpPZDcGzN
# R95QlryjfEjtl+gxj12zNPEdADPplVfzt8cYRWFBx/Fbfch08k6P9p7jX2q1jFPb
# UxWYJ+xOyGC1aKhDGY5b+8wL39v6qC0HFIx/v3y+bep+aEXooK8VoeWK+szfaFjX
# o8YTcvQ8UL4szu9HFTuZNv6vvoJ7Ju+o5aTj51sph+0+FXW38TlL/rDBd5ia79js
# kLtOeHbDjkbljilwzegcxv9i49F05ZrS/5ELZCCY1VaqO7EOLKVaxxdAO5oy1vb0
# Bx0ZRVX1mxFjYzay2EC051k6yGJHm58y1oe2IKRa/SM1+BTGse6vHNi5Q2d5ZnoR
# 9AOAUDDwJIIqRI4rZz2MSinh11WrXTG9urF2uoyd5Ve+8hxes9ABeP2PYQKlXYTA
# xvdaeanDTQ/vwmnM+yTcWzrVm84Z38XVFw4G7p/ZNZ2nscvv6uru2AevXcyV1t8h
# a7iWmhhgTWBNBrViuDlc3iPvOz2SVPbPeqhyY/NXwNZCAgc2H5pOztu6MwQxDIjt
# e3XM/FkKBxHofS2abNT/0HG+xZtFqUJDaxgbJa6lN1zh7spjuQ8CAwEAAaOCAcsw
# ggHHMB0GA1UdDgQWBBRWBF8QbdwIA/DIv6nJFsrB16xltjAfBgNVHSMEGDAWgBRr
# aSg6NS9IY0DPe9ivSek+2T3bITBsBgNVHR8EZTBjMGGgX6BdhltodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBQdWJsaWMlMjBS
# U0ElMjBUaW1lc3RhbXBpbmclMjBDQSUyMDIwMjAuY3JsMHkGCCsGAQUFBwEBBG0w
# azBpBggrBgEFBQcwAoZdaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# ZXJ0cy9NaWNyb3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3RhbXBpbmclMjBD
# QSUyMDIwMjAuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
# AwgwDgYDVR0PAQH/BAQDAgeAMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yDfQEBMEEw
# PwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9j
# cy9SZXBvc2l0b3J5Lmh0bTAIBgZngQwBBAIwDQYJKoZIhvcNAQEMBQADggIBAFIe
# 4ZJUe9qUKcWeWypchB58fXE/ZIWv2D5XP5/k/tB7LCN9BvmNSVKZ3VeclQM978wf
# EvuvdMQSUv6Y20boIM8DK1K1IU9cP21MG0ExiHxaqjrikf2qbfrXIip4Ef3v2bNY
# KQxCxN3Sczp1SX0H7uqK2L5OhfDEiXf15iou5hh+EPaaqp49czNQpJDOR/vfJghU
# c/qcslDPhoCZpZx8b2ODvywGQNXwqlbsmCS24uGmEkQ3UH5JUeN6c91yasVchS78
# riMrm6R9ZpAiO5pfNKMGU2MLm1A3pp098DcbFTAc95Hh6Qvkh//28F/Xe2bMFb6D
# L7Sw0ZO95v0gv0ZTyJfxS/LCxfraeEII9FSFOKAMEp1zNFSs2ue0GGjBt9yEEMUw
# vxq9ExFz0aZzYm8ivJfffpIVDnX/+rVRTYcxIkQyFYslIhYlWF9SjCw5r49qakjM
# RNh8W9O7aaoolSVZleQZjGt0K8JzMlyp6hp2lbW6XqRx2cOHbbxJDxmENzohGUzi
# I13lI2g2Bf5qibfC4bKNRpJo9lbE8HUbY0qJiE8u3SU8eDQaySPXOEhJjxRCQwwO
# vejYmBG5P7CckQNBSnnl12+FKRKgPoj0Mv+z5OMhj9z2MtpbnHLAkep0odQClEyy
# CG/uR5tK5rW6mZH5Oq56UWS0NI6NV1JGS7Jri6jFMYIHQzCCBz8CAQEweDBhMQsw
# CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD
# VQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAIT
# MwAAAFXZ3WkmKPn44gAAAAAAVTANBglghkgBZQMEAgEFAKCCBJwwEQYLKoZIhvcN
# AQkQAg8xAgUAMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0B
# CQUxDxcNMjYwNjMwMjA0ODEwWjAvBgkqhkiG9w0BCQQxIgQgjR+46RNzywptmjZk
# vJE31wtN0mvXRVqEkxrp1hRufKYwgbkGCyqGSIb3DQEJEAIvMYGpMIGmMIGjMIGg
# BCDYuTyXZIZiu799/v4PaqsmeSzBxh0rqkYq7sYYavj+zTB8MGWkYzBhMQswCQYD
# VQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQD
# EylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAITMwAA
# AFXZ3WkmKPn44gAAAAAAVTCCA14GCyqGSIb3DQEJEAISMYIDTTCCA0mhggNFMIID
# QTCCAikCAQEwggEJoYHhpIHeMIHbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0QwMC0wNUUwLUQ5NDcxNTAzBgNV
# BAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5
# oiMKAQEwBwYFKw4DAhoDFQAdO1QBgmW/tuBZV5EGjhfsV4cN6qBnMGWkYzBhMQsw
# CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD
# VQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDAN
# BgkqhkiG9w0BAQsFAAIFAO3un7AwIhgPMjAyNjA2MzAyMDA0MDBaGA8yMDI2MDcw
# MTIwMDQwMFowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA7e6fsAIBADAHAgEAAgI7
# 0DAHAgEAAgISNTAKAgUA7e/xMAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEE
# AYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IB
# AQBzqQ0aM9Zvj228kG/3F8/1DhTggH+t3bsQRq3NiTDikKdaNlPl5FYG3kbqbI7U
# AN/KqapqGL3CYHD8A91gkugF/I2C+b19T4HnOpWbXuimAPMfR5RFwBwmRte9yDad
# YDAMNpwEH+o0m90P9/++LTlNSARXuBQQdWJ16X3ONr3z7dRMtAx9P0S4CiIawcPW
# sWZtqIpdNrfwRkI+bjLmsyavz3Btu2lEiKS6Z4l6WchBNeXRbHO6ndub7rBdRTpv
# qJ1MXna/QmNS7rOtiJnLOHkjBxEgoEklhCWqQjQNYUzrLNb8IWRH2ksOSNCmk8J/
# lzqdcRxDeDXFzXiEwZrJZyg7MA0GCSqGSIb3DQEBAQUABIICAGdqeHyTvEB+Wv9g
# gSEKkcfChpBAuIEyDVY4uVFvsvJNuSoFl9OQmvw8eds7T3nzrwFS83CYJ9d4yCok
# btU9w+qUIjjSQjG3xaJuW20RohD2y09sb5nd5wRzgMPNOJMViAEAO1/k7b/4MeP7
# 2mFF9MCAgxYiAdPqVUO7eKo7uZFdv/j/q10lPtTV/ySY0XnVlGjJmPk4GaR7WjFt
# kWXxEPof+0pWo9Jtmbl7u55tEAsOpJN2iuoJJBNdfYu8lJAD3Etm8XdoqewfaGyN
# HrOF/q37DPnZWLMoXh6w1Bc0MFFPlxjHAqshFy0y4gfmo7A5RSkQ2WVFDNAyXnpo
# ukop/JjiDk/zjpg24bRBG4X1xm5+EwNLvBq+JBxuhbal61F05AFBLJVmML8A9jpu
# ztMMNYfa8SBVTFcf5d12xiatMT0MTfaqksFg+F2XacvNd/DAS5uDJFaJO3klMa5w
# 2kr+u28RXYRGA5t3Nbb7aXfmuOraRaHCG/wNx6Nl3wQcF/eM2VPuhoPS4GQCseQ6
# We1ySYlAEBgtlr615GJIIFYs5TPQbvYNo3P1e9FOSHyiD6i8V00wLYuy8H8NJU+O
# Iwbv6sY8L/1eg10d8FQ/SEGeIXULoI0oINfVzCO7Dd6jFhpwYxu0tjAX78wR4tv3
# JwjSef9KwJ+74atzYQbFACxI1xe3
# SIG # End signature block