oob.psm1

function Start-OOB {
<#
.SYNOPSIS
    Launch an Out-of-Band listener (Node) with a Cloudflare tunnel.
 
.PARAMETER ResponseType
    What the listener should return. Defaults to **None** (204).
 
        None → 204 No Content (stealth beacon)
        Image → Serve a static file (PNG/GIF/ICO…)
        File → Serve any file (PDF/TXT/ZIP…)
        Redirect → 302 Location: <url>
        Script → application/javascript body
 
.PARAMETER ResponseFile
    Path to the file to serve (required for Image / File).
 
.PARAMETER RedirectUrl
    Where to send the browser on a 302 (required for Redirect).
 
.PARAMETER ScriptBody
    Inline JavaScript text (required for Script).
 
.PARAMETER ContentType
    Override Content-Type when using ResponseFile/File (optional).
 
#>

    [CmdletBinding()]
    param (
        [int]    $Port    = 8000,
        [string] $Name    = 'oob',
        [string] $Path    = '/',
        [int]    $Timeout = 15,

        [switch] $Persistent,
        [switch] $Tail,

        [ValidateSet('None','Image','File','Redirect','Script')]
        [string] $ResponseType = 'None',

        [string] $ResponseFile,
        [string] $RedirectUrl,
        [string] $ScriptBody,
        [string] $ContentType
    )

    # ── env checks ──────────────────────────────────────────────────
    Import-Module Node        -ErrorAction Stop; Assert-NodeInstalled
    Import-Module cloudflared -ErrorAction Stop
    Write-Host "ℹ️ Node : $(Get-NodePath)"

    # ── script & args ───────────────────────────────────────────────
    if (-not $Path.StartsWith('/')) { $Path = "/$Path" }

    $jsPath = Join-Path $PSScriptRoot 'oob-server.js'

    $nodeArgs = @(
        "`"$jsPath`"",
        "--port"        , $Port,
        "--name"        , $Name,
        "--path"        , $Path,
        "--response-type", $ResponseType.ToLower()
    )

    switch ($ResponseType) {
        'Image'   { $nodeArgs += @('--response-file', $ResponseFile) }
        'File'    { $nodeArgs += @('--response-file', $ResponseFile) }
        'Redirect'{ $nodeArgs += @('--redirect-url' , $RedirectUrl ) }
        'Script'  { $nodeArgs += @('--script-body'  , $ScriptBody ) }
    }
    if ($ContentType) { $nodeArgs += @('--content-type', $ContentType) }

    # ── launch Node listener (hidden if persistent without -Tail) ──
    $windowStyle = if ($Persistent -and -not $Tail) { 'Hidden' } else { 'Normal' }
    if (-not $script:__oobProc -or $script:__oobProc.HasExited) {
        $script:__oobProc = Start-Process node `
            -ArgumentList $nodeArgs `
            -WindowStyle $windowStyle `
            -PassThru
        Start-Sleep 1
    }

    # ── tunnel handling (unchanged) ─────────────────────────────────
    if (-not $Persistent) {
        $tName              = "${Name}_$(Get-Random -min 1000 -max 9999)"
        $script:__oobTunnel = Start-TempTunnel -Name $tName -Url "http://localhost:$Port" -TimeoutSeconds $Timeout
        Write-Host "🌐 Temp tunnel : $($script:__oobTunnel.Url)"
    } else {
        $script:__oobTunnel = Start-PersistentTunnel -Name $Name
        Write-Host "🌐 Persist URL : $($script:__oobTunnel.Url)"
    }

    # ── (tail-log logic unchanged) … ────────────────────────────────
    # -- snipped for brevity --
}



function Stop-OOB {
    <#
    .SYNOPSIS
        Cleanly tear down anything Start-OOB created:
            • cloudflared tunnel (temp or persistent)
            • node listener
            • log-tail job
            • any child pwsh wrapper hosting cloudflared
    #>

    [CmdletBinding()]
    param()

    # ── 1. Tunnel teardown ───────────────────────────────────────────
    if ($script:__oobTunnel) {
        try {
            switch ($script:__oobTunnel.Type) {
                'Temporary'  { Stop-TempTunnel       -Name $script:__oobTunnel.Name -ErrorAction SilentlyContinue }
                'Persistent' { Stop-PersistentTunnel -Name $script:__oobTunnel.Name -ErrorAction SilentlyContinue }
                default      { 
                    # If Type is missing or unexpected, attempt both
                    Stop-TempTunnel       -Name $script:__oobTunnel.Name -ErrorAction SilentlyContinue
                    Stop-PersistentTunnel -Name $script:__oobTunnel.Name -ErrorAction SilentlyContinue
                }
            }
        } catch { }
        $script:__oobTunnel = $null
    }

    # ── 2. Kill the node listener ────────────────────────────────────
    if ($script:__oobProc -and -not $script:__oobProc.HasExited) {
        try {
            Stop-Process -Id $script:__oobProc.Id -Force -ErrorAction SilentlyContinue
        } catch { }
        $script:__oobProc = $null
    }

    # ── 3. Stop & remove the tail job ────────────────────────────────
    if ($script:__logJob) {
        try {
            Stop-Job   -Job $script:__logJob -Force -ErrorAction SilentlyContinue
            Remove-Job -Job $script:__logJob -Force -ErrorAction SilentlyContinue
        } catch { }
        $script:__logJob = $null
    }

    # ── 4. Kill any child pwsh wrappers hosting cloudflared ──────────
    # (Start-PersistentTunnel launches a child pwsh process)
    try {
        Get-CimInstance Win32_Process -Filter "Name='pwsh.exe'" |
            Where-Object { $_.ParentProcessId -eq $PID } |
            ForEach-Object {
                Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
            }
    } catch { }

    Write-Host "🛑 OOB listener, tunnel, and helper processes stopped."
}



Export-ModuleMember -Function Start-OOB, Stop-OOB