Public/Connect-NMMHiddenApi.ps1

function Connect-NMMHiddenApi {
    <#
    .SYNOPSIS
        Start HTTP listener and open browser for NMM Hidden API authentication.
    .DESCRIPTION
        Starts a local HTTP server that listens for cookies from the NMM-PS browser extension.
        Opens the NMM portal in your default browser where you can log in, then click the
        extension button to send cookies back to PowerShell.
 
        Workflow:
        1. Run Connect-NMMHiddenApi
        2. Log into NMM in the browser that opens
        3. Click the NMM-PS extension icon
        4. Click "Send Cookies to PowerShell"
        5. The function completes and you can use Invoke-HiddenApiRequest
 
        Requires the NMM-PS browser extension to be installed.
        See BrowserExtension/README.md for installation instructions.
    .PARAMETER Port
        The port to listen on for cookie delivery. Default: 19847
    .PARAMETER Timeout
        How long to wait for cookies in seconds. Default: 120 (2 minutes)
    .PARAMETER NoBrowser
        Don't open the browser automatically. Use this if you're already logged in.
    .PARAMETER BaseUri
        Override the NMM portal URL. If not specified, reads from ConfigData.json.
    .EXAMPLE
        Connect-NMMHiddenApi
 
        Opens browser to NMM portal, waits for extension to send cookies.
    .EXAMPLE
        Connect-NMMHiddenApi -NoBrowser
 
        Just starts the listener without opening a browser.
    .EXAMPLE
        Connect-NMMHiddenApi -Timeout 300
 
        Waits up to 5 minutes for cookies.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [int]$Port = 19847,

        [Parameter()]
        [int]$Timeout = 120,

        [Parameter()]
        [switch]$NoBrowser,

        [Parameter()]
        [string]$BaseUri
    )

    process {
        # Determine NMM portal URL
        if ([string]::IsNullOrEmpty($BaseUri)) {
            try {
                $config = Get-ConfigData -ErrorAction Stop
                if ($config.BaseUri) {
                    $BaseUri = $config.BaseUri.TrimEnd('/')
                }
                elseif ($config.HiddenApiBaseUri) {
                    # Extract base domain from API URL
                    $uri = [System.Uri]$config.HiddenApiBaseUri
                    $BaseUri = "$($uri.Scheme)://$($uri.Host)"
                }
            }
            catch {
                Write-Warning "Could not read ConfigData.json. Please specify -BaseUri parameter."
                return
            }
        }

        if ([string]::IsNullOrEmpty($BaseUri)) {
            Write-Error "No NMM portal URL found. Specify -BaseUri or configure BaseUri in ConfigData.json"
            return
        }

        Write-Host ""
        Write-Host " NMM Hidden API Authentication" -ForegroundColor Cyan
        Write-Host " ─────────────────────────────" -ForegroundColor DarkGray
        Write-Host ""

        # Create HTTP listener
        $listener = New-Object System.Net.HttpListener
        $prefix = "http://localhost:$Port/"

        try {
            $listener.Prefixes.Add($prefix)
            $listener.Start()
            Write-Host " [1/3] " -ForegroundColor DarkGray -NoNewline
            Write-Host "Listening on port $Port" -ForegroundColor Green
        }
        catch {
            if ($_.Exception.Message -like "*access*denied*" -or $_.Exception.Message -like "*permission*") {
                Write-Error "Cannot bind to port $Port. Try a different port with -Port parameter."
            }
            else {
                Write-Error "Failed to start HTTP listener: $($_.Exception.Message)"
            }
            return
        }

        # Open browser
        if (-not $NoBrowser) {
            Write-Host " [2/3] " -ForegroundColor DarkGray -NoNewline
            Write-Host "Opening browser to: " -ForegroundColor Yellow -NoNewline
            Write-Host $BaseUri -ForegroundColor White

            try {
                if ($IsMacOS) {
                    Start-Process "open" -ArgumentList $BaseUri
                }
                elseif ($IsLinux) {
                    Start-Process "xdg-open" -ArgumentList $BaseUri
                }
                else {
                    Start-Process $BaseUri
                }
            }
            catch {
                Write-Warning "Could not open browser. Please navigate to $BaseUri manually."
            }
        }
        else {
            Write-Host " [2/3] " -ForegroundColor DarkGray -NoNewline
            Write-Host "Browser not opened (-NoBrowser specified)" -ForegroundColor Yellow
        }

        # Extract domain from BaseUri for status endpoint
        $expectedDomain = ([System.Uri]$BaseUri).Host

        Write-Host " [3/3] " -ForegroundColor DarkGray -NoNewline
        Write-Host "Waiting for cookies (auto-send enabled)..." -ForegroundColor Yellow
        Write-Host ""
        Write-Host " Expected domain: " -ForegroundColor DarkGray -NoNewline
        Write-Host $expectedDomain -ForegroundColor White
        Write-Host " Timeout: $Timeout seconds" -ForegroundColor DarkGray
        Write-Host ""
        Write-Host " The extension will auto-send cookies when you log in." -ForegroundColor Gray
        Write-Host " Or click the extension icon to send manually." -ForegroundColor Gray
        Write-Host ""

        # Request handling loop - handles /status and /cookies
        $startTime = Get-Date
        $cookiesReceived = $false

        while (-not $cookiesReceived) {
            # Check timeout
            $elapsed = (Get-Date) - $startTime
            if ($elapsed.TotalSeconds -ge $Timeout) {
                $listener.Stop()
                Write-Warning "Timeout waiting for cookies. Make sure:"
                Write-Host " - The NMM-PS extension is installed" -ForegroundColor Yellow
                Write-Host " - You're logged into NMM at $expectedDomain" -ForegroundColor Yellow
                return
            }

            # Calculate remaining time for this iteration
            $remainingMs = [int](($Timeout - $elapsed.TotalSeconds) * 1000)
            if ($remainingMs -le 0) { $remainingMs = 1000 }
            if ($remainingMs -gt 5000) { $remainingMs = 5000 }  # Check every 5 seconds max

            $timeoutTask = [System.Threading.Tasks.Task]::Delay($remainingMs)
            $contextTask = $listener.GetContextAsync()

            $completedTask = [System.Threading.Tasks.Task]::WhenAny($contextTask, $timeoutTask).GetAwaiter().GetResult()

            if ($completedTask -eq $timeoutTask) {
                # No request yet, continue waiting
                continue
            }

            # Process the request
            $context = $contextTask.GetAwaiter().GetResult()
            $request = $context.Request
            $response = $context.Response

            # Add CORS headers
            $response.Headers.Add("Access-Control-Allow-Origin", "*")
            $response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
            $response.Headers.Add("Access-Control-Allow-Headers", "Content-Type")

            # Handle OPTIONS preflight
            if ($request.HttpMethod -eq "OPTIONS") {
                $response.StatusCode = 200
                $response.Close()
                continue
            }

            # Handle GET /status - returns listener info for auto-send
            if ($request.HttpMethod -eq "GET" -and $request.Url.LocalPath -eq "/status") {
                $statusJson = @{
                    listening = $true
                    domain    = $expectedDomain
                    port      = $Port
                    baseUri   = $BaseUri
                } | ConvertTo-Json

                $buffer = [System.Text.Encoding]::UTF8.GetBytes($statusJson)
                $response.ContentType = "application/json"
                $response.ContentLength64 = $buffer.Length
                $response.StatusCode = 200
                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                $response.Close()

                Write-Verbose "Status check from extension"
                continue
            }

            # Handle POST /cookies
            if ($request.HttpMethod -eq "POST" -and $request.Url.LocalPath -eq "/cookies") {
                # Read request body
                $reader = New-Object System.IO.StreamReader($request.InputStream)
                $body = $reader.ReadToEnd()
                $reader.Close()

                try {
                    $data = $body | ConvertFrom-Json

                    if ($data.cookies) {
                        # Store cookies using Set-NMMHiddenApiCookie logic
                        $cookies = @{}
                        $data.cookies -split ';' | ForEach-Object {
                            $trimmed = $_.Trim()
                            if ($trimmed -match '^([^=]+)=(.+)$') {
                                $name = $matches[1].Trim()
                                $value = $matches[2].Trim()
                                if ($name -and $value) {
                                    $cookies[$name] = $value
                                }
                            }
                        }

                        if ($cookies.Count -gt 0) {
                            $Script:HiddenApiCookies = $cookies
                            $Script:HiddenApiAuthMethod = 'Cookie'

                            # Send success response
                            $responseJson = @{
                                success     = $true
                                message     = "Cookies received"
                                cookieCount = $cookies.Count
                            } | ConvertTo-Json

                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($responseJson)
                            $response.ContentType = "application/json"
                            $response.ContentLength64 = $buffer.Length
                            $response.StatusCode = 200
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()

                            $cookiesReceived = $true
                            $listener.Stop()

                            Write-Host " ✓ " -ForegroundColor Green -NoNewline
                            Write-Host "Received $($cookies.Count) cookies from extension" -ForegroundColor White
                            Write-Host ""
                            Write-Host " Cookies: " -ForegroundColor Gray -NoNewline
                            Write-Host ($cookies.Keys -join ', ') -ForegroundColor DarkGray
                            Write-Host ""
                            Write-Host " You can now use " -ForegroundColor Gray -NoNewline
                            Write-Host "Invoke-HiddenApiRequest" -ForegroundColor Cyan -NoNewline
                            Write-Host " to call APIs." -ForegroundColor Gray
                            Write-Host ""

                            return [PSCustomObject]@{
                                Success     = $true
                                AuthMethod  = 'Cookie'
                                CookieCount = $cookies.Count
                                CookieNames = [array]$cookies.Keys
                                Domain      = $data.domain
                            }
                        }
                    }

                    # No cookies in request
                    $errorJson = @{ success = $false; message = "No cookies in request" } | ConvertTo-Json
                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($errorJson)
                    $response.ContentType = "application/json"
                    $response.StatusCode = 400
                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                    $response.Close()
                    # Don't stop listener - continue waiting for valid cookies
                    Write-Warning "Received request but no cookies were included. Still waiting..."
                    continue
                }
                catch {
                    $errorJson = @{ success = $false; message = "Invalid request" } | ConvertTo-Json
                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($errorJson)
                    $response.ContentType = "application/json"
                    $response.StatusCode = 400
                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                    $response.Close()
                    Write-Warning "Failed to parse cookie data: $($_.Exception.Message)"
                    continue
                }
            }
            else {
                # Unknown endpoint - return 404 but keep listening
                $response.StatusCode = 404
                $response.Close()
                Write-Verbose "Unknown request: $($request.HttpMethod) $($request.Url.LocalPath)"
                continue
            }
        }
    }
}