scripts/internal/bootstrap/HostEventAdapter.ps1

<#
.SYNOPSIS
  Read the standard turn record the hook dispatcher hands the bootstrap, and keep session ids path-safe.
.DESCRIPTION
  Feature 202 T002 (FR-006, DA-1 option B): the hook dispatcher is the ONLY component that reads a host
  payload. It builds the standard turn record from the host's declared binding (hosts/<host>/host.psd1) and
  passes it to the bootstrap provider as --turn-record. This adapter only decodes and validates that record;
  it never normalizes a raw host payload. (Before 202 it did, with its own key list that accepted an `id` key
  the dispatcher ignored, so the two disagreed about the session.) Pure transform - no filesystem or git access.
.OUTPUTS
  [pscustomobject] the turn record, or $null when the argument is absent or not a valid record.
#>


function New-SpecrewPerLaunchSessionToken {
    return ('launch-{0}' -f ([guid]::NewGuid().ToString('N')))
}

function ConvertTo-SpecrewFilesystemSafeSessionId {
    param([AllowNull()][string]$RawSessionId)

    if ([string]::IsNullOrWhiteSpace($RawSessionId)) {
        return (New-SpecrewPerLaunchSessionToken)
    }

    $clean = ([string]$RawSessionId) -replace '[^a-zA-Z0-9-]+', '-'
    $clean = $clean.Trim('-')
    if ([string]::IsNullOrWhiteSpace($clean)) {
        return (New-SpecrewPerLaunchSessionToken)
    }
    return $clean
}

function ConvertFrom-SpecrewTurnRecordArgument {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        # The dispatcher's --turn-record value: base64 of the record's UTF-8 JSON. Empty or garbage yields $null.
        [Parameter(Mandatory)][AllowEmptyString()][string] $Encoded
    )

    if ([string]::IsNullOrWhiteSpace($Encoded)) { return $null }
    try {
        $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Encoded.Trim()))
        $record = $json | ConvertFrom-Json -ErrorAction Stop
    }
    catch { return $null }
    if ($null -eq $record -or $record -isnot [psobject]) { return $null }
    $schema = $record.PSObject.Properties['schema']
    if (-not $schema -or [string]$schema.Value -cne 'specrew-turn-record/v1') { return $null }
    # The safe id is re-derived here rather than trusted: it becomes part of a filename.
    $rawIdProperty = $record.PSObject.Properties['session_id']
    $safeProperty = $record.PSObject.Properties['safe_session_id']
    $rawId = if ($rawIdProperty) { [string]$rawIdProperty.Value } else { '' }
    $safe = if ($safeProperty) { [string]$safeProperty.Value } else { '' }
    if ([string]::IsNullOrWhiteSpace($rawId)) {
        if ([string]::IsNullOrWhiteSpace($safe) -or $safe -notmatch '^launch-[0-9a-f]{32}$') { $safe = New-SpecrewPerLaunchSessionToken }
    }
    else {
        $safe = ConvertTo-SpecrewFilesystemSafeSessionId -RawSessionId $rawId
    }
    $record | Add-Member -NotePropertyName safe_session_id -NotePropertyValue $safe -Force
    return $record
}