Support/Package/Schema/Eigenverft.Manifested.Sandbox.Package.PublisherInventory.Management.ps1

<#
    Eigenverft.Manifested.Sandbox.Package - PackagePublisherInventory.json management helpers.
#>


function Assert-PackagePublisherId {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PublisherId
    )

    if ([string]::IsNullOrWhiteSpace($PublisherId)) {
        throw 'Package publisher id must not be empty.'
    }
    if ($PublisherId -notmatch '^[A-Za-z][A-Za-z0-9_.-]*( [A-Za-z0-9_.-]+)*$') {
        throw "Package publisher '$PublisherId' is invalid. Use letters, numbers, spaces, '.', '-' or '_' and start with a letter. Spaces must separate non-empty words."
    }
}

function Get-PackagePublisherEntries {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Document
    )

    if (-not $Document.PSObject.Properties['publishers'] -or $null -eq $Document.publishers) {
        return @()
    }
    if ($Document.publishers -isnot [System.Array]) {
        throw "Package publisher inventory must define publishers as an array of objects with publisherId."
    }

    return @($Document.publishers)
}

function Assert-PackagePublisherInventorySchema {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PublisherInventoryDocumentInfo
    )

    $document = $PublisherInventoryDocumentInfo.Document
    if (-not $document.PSObject.Properties['inventoryVersion']) {
        throw "Package publisher inventory '$($PublisherInventoryDocumentInfo.Path)' is missing inventoryVersion."
    }
    if (-not $document.PSObject.Properties['publishers'] -or $null -eq $document.publishers) {
        throw "Package publisher inventory '$($PublisherInventoryDocumentInfo.Path)' is missing publishers."
    }

    $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($publisher in @(Get-PackagePublisherEntries -Document $document)) {
        foreach ($requiredProperty in @('publisherId', 'publisherName', 'enabled', 'trusted', 'trustMode')) {
            if (-not $publisher.PSObject.Properties[$requiredProperty] -or
                ($requiredProperty -in @('publisherId', 'publisherName', 'trustMode') -and [string]::IsNullOrWhiteSpace([string]$publisher.$requiredProperty))) {
                throw "Package publisher inventory '$($PublisherInventoryDocumentInfo.Path)' has a publisher missing '$requiredProperty'."
            }
        }
        if ($publisher.PSObject.Properties['searchOrder']) {
            throw "Package publisher '$($publisher.publisherId)' in '$($PublisherInventoryDocumentInfo.Path)' still uses retired property 'searchOrder'. Publisher trust is permission, not ranking; endpoint searchOrder controls source precedence."
        }

        $publisherId = [string]$publisher.publisherId
        if (-not $seen.Add($publisherId)) {
            throw "Package publisher inventory '$($PublisherInventoryDocumentInfo.Path)' defines duplicate publisherId '$publisherId'."
        }
        Assert-PackagePublisherId -PublisherId $publisherId

        $trustMode = [string]$publisher.trustMode
        if ($trustMode -notin @('moduleShipped', 'unsigned', 'unsignedExplicit')) {
            throw "Package publisher '$publisherId' in '$($PublisherInventoryDocumentInfo.Path)' has unsupported trustMode '$trustMode'."
        }
        if ([string]::Equals($trustMode, 'moduleShipped', [System.StringComparison]::OrdinalIgnoreCase) -and -not [bool]$publisher.trusted) {
            throw "Package publisher '$publisherId' uses trustMode='moduleShipped' but trusted is false."
        }
        if ([string]::Equals($trustMode, 'unsigned', [System.StringComparison]::OrdinalIgnoreCase) -and [bool]$publisher.trusted) {
            throw "Package publisher '$publisherId' uses trustMode='unsigned' but trusted is true."
        }
        if ([string]::Equals($trustMode, 'unsignedExplicit', [System.StringComparison]::OrdinalIgnoreCase) -and -not [bool]$publisher.trusted) {
            throw "Package publisher '$publisherId' uses trustMode='unsignedExplicit' but trusted is false."
        }
    }
}

function Get-PackagePublisherInventoryInfo {
    [CmdletBinding()]
    param()

    $inventoryPath = Get-PackagePublisherInventoryPath
    $documentInfo = Read-PackageJsonDocument -Path $inventoryPath
    Assert-PackagePublisherInventorySchema -PublisherInventoryDocumentInfo $documentInfo
    $documentInfo | Add-Member -MemberType NoteProperty -Name Exists -Value $true -Force
    return $documentInfo
}

function Get-PackagePublisherInventoryEditInfo {
    [CmdletBinding()]
    param()

    $documentInfo = Get-PackagePublisherInventoryInfo
    if (-not $documentInfo.Document.PSObject.Properties['publishers'] -or $null -eq $documentInfo.Document.publishers) {
        $documentInfo.Document | Add-Member -MemberType NoteProperty -Name 'publishers' -Value @() -Force
    }
    return $documentInfo
}

function Save-PackagePublisherInventoryDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$DocumentInfo
    )

    Assert-PackagePublisherInventorySchema -PublisherInventoryDocumentInfo $DocumentInfo

    $directory = Split-Path -Parent $DocumentInfo.Path
    if (-not [string]::IsNullOrWhiteSpace($directory)) {
        $null = New-Item -ItemType Directory -Path $directory -Force
    }

    $temporaryPath = '{0}.{1}.tmp' -f $DocumentInfo.Path, ([guid]::NewGuid().ToString('N'))
    try {
        $DocumentInfo.Document | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath $temporaryPath -Encoding UTF8
        Move-Item -LiteralPath $temporaryPath -Destination $DocumentInfo.Path -Force
    }
    finally {
        if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) {
            Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
        }
    }
}

function Get-PackagePublisherProperty {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Document,

        [Parameter(Mandatory = $true)]
        [string]$PublisherId
    )

    foreach ($publisher in @(Get-PackagePublisherEntries -Document $Document)) {
        if ($publisher.PSObject.Properties['publisherId'] -and
            [string]::Equals([string]$publisher.publisherId, $PublisherId, [System.StringComparison]::OrdinalIgnoreCase)) {
            return [pscustomobject]@{
                Name  = [string]$publisher.publisherId
                Value = $publisher
            }
        }
    }

    return $null
}

function New-PackagePublisherEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PublisherId,

        [AllowNull()]
        [string]$PublisherName = $null,

        [bool]$Enabled = $true,

        [bool]$Trusted = $false,

        [string]$TrustMode = 'unsigned',

        [AllowNull()]
        [string]$TrustReason = $null
    )

    Assert-PackagePublisherId -PublisherId $PublisherId
    if ([string]::IsNullOrWhiteSpace($PublisherName)) {
        $PublisherName = $PublisherId
    }

    $entry = [ordered]@{
        publisherId   = $PublisherId
        publisherName = $PublisherName
        enabled       = $Enabled
        trusted       = $Trusted
        trustMode     = $TrustMode
    }
    if ($Trusted) {
        $entry['trustedAtUtc'] = [DateTime]::UtcNow.ToString('o')
        if (-not [string]::IsNullOrWhiteSpace($TrustReason)) {
            $entry['trustReason'] = $TrustReason
        }
    }

    return [pscustomobject]$entry
}

function Get-PackageEnabledTrustedPublisherRows {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PublisherInventoryDocument
    )

    $rows = @(
        foreach ($publisher in @(Get-PackagePublisherEntries -Document $PublisherInventoryDocument)) {
            if (-not [bool]$publisher.enabled) {
                continue
            }
            if (-not [bool]$publisher.trusted) {
                continue
            }

            [pscustomobject]@{
                PublisherId   = [string]$publisher.publisherId
                PublisherName = [string]$publisher.publisherName
                TrustMode     = [string]$publisher.trustMode
                Source        = $publisher
            }
        }
    )

    return @($rows | Sort-Object -Property PublisherId)
}

function Select-PackagePublisherSummary {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Publisher,

        [Parameter(Mandatory = $true)]
        [string]$InventoryPath
    )

    $enabled = if ($Publisher.PSObject.Properties['enabled']) { [bool]$Publisher.enabled } else { $true }
    $trusted = if ($Publisher.PSObject.Properties['trusted']) { [bool]$Publisher.trusted } else { $false }
    $trustMode = if ($Publisher.PSObject.Properties['trustMode']) { [string]$Publisher.trustMode } else { 'unsigned' }
    $notes = New-Object System.Collections.Generic.List[string]
    if (-not $enabled) {
        $notes.Add('Disabled; matching definitions from this publisher are ignored.') | Out-Null
    }
    if (-not $trusted) {
        $notes.Add('Untrusted; matching definitions from this publisher cannot be executed.') | Out-Null
    }
    if ($trusted -and [string]::Equals($trustMode, 'unsignedExplicit', [System.StringComparison]::OrdinalIgnoreCase)) {
        $notes.Add('Unsigned definitions are trusted by explicit local publisher policy.') | Out-Null
    }

    return [pscustomobject]@{
        PublisherId   = [string]$Publisher.publisherId
        PublisherName = [string]$Publisher.publisherName
        Enabled       = $enabled
        Trusted       = $trusted
        TrustMode     = $trustMode
        InventoryPath = $InventoryPath
        TrustedAtUtc  = if ($Publisher.PSObject.Properties['trustedAtUtc']) { [string]$Publisher.trustedAtUtc } else { $null }
        TrustReason   = if ($Publisher.PSObject.Properties['trustReason']) { [string]$Publisher.trustReason } else { $null }
        Notes         = @($notes.ToArray())
    }
}

function Get-PackagePublisherSummaries {
    [CmdletBinding()]
    param()

    $documentInfo = Get-PackagePublisherInventoryEditInfo
    foreach ($publisher in @(Get-PackagePublisherEntries -Document $documentInfo.Document)) {
        Select-PackagePublisherSummary -Publisher $publisher -InventoryPath $documentInfo.Path
    }
}

function New-PackagePublisherCommandResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Action,

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

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

        [AllowNull()]
        [psobject]$Before,

        [AllowNull()]
        [psobject]$After,

        [string]$Status = 'Updated',

        [string[]]$Notes = @()
    )

    foreach ($note in @($Notes)) {
        if (-not [string]::IsNullOrWhiteSpace($note)) {
            Write-Warning $note
        }
    }

    return [pscustomobject]@{
        Action        = $Action
        PublisherId   = $PublisherId
        InventoryPath = $InventoryPath
        Status        = $Status
        Before        = $Before
        After         = $After
        Notes         = @($Notes)
    }
}