Support/Package/Schema/Eigenverft.Manifested.Package.Package.DefinitionSchema.ps1
|
<#
Eigenverft.Manifested.Package.Package.DefinitionSchema Package definition JSON validation for package definition wire models. Runtime validation is PowerShell-only (this module + DefinitionSchema.Wire1_8.ps1). The JSON schema file is the editor/agent contract (canonical examples under Endpoint/Defaults); keep schema and asserts aligned. Schema 1.8 root description and x-eigenverftAgentHint tell LLMs to author kind=unsigned drafts first and run Sign-PackageDefinition after content is final; runtime ignores those hints. #> $script:PackageDefinitionSupportedSchemaVersions = @( '1.8' ) function Assert-PackageDefinitionSchemaVersionSupported { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SchemaVersionText, [Parameter(Mandatory = $true)] [string]$DefinitionDocumentPath ) foreach ($supported in $script:PackageDefinitionSupportedSchemaVersions) { if ([string]::Equals($SchemaVersionText, $supported, [System.StringComparison]::Ordinal)) { return } } $supportedList = ($script:PackageDefinitionSupportedSchemaVersions | ForEach-Object { "'$_'" }) -join ', ' throw "Package definition '$DefinitionDocumentPath' uses unsupported schemaVersion '$SchemaVersionText'. Supported schemaVersion values are $supportedList." } function Assert-PackageDefinitionSignatureSchema_1_8 { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$DefinitionDocumentInfo, [Parameter(Mandatory = $true)] [string]$DefinitionId ) $definition = $DefinitionDocumentInfo.Document $publication = $definition.definitionPublication if (-not $publication.PSObject.Properties['definitionSignature'] -or -not $publication.definitionSignature) { throw "Package definition '$DefinitionId' schemaVersion 1.8 is missing definitionPublication.definitionSignature." } $signature = $publication.definitionSignature foreach ($requiredProperty in @('kind', 'format', 'signedContent')) { if (-not $signature.PSObject.Properties[$requiredProperty] -or [string]::IsNullOrWhiteSpace([string]$signature.$requiredProperty)) { throw "Package definition '$DefinitionId' definitionSignature is missing '$requiredProperty'." } } $kind = [string]$signature.kind if ($kind -notin @('signed', 'unsigned')) { throw "Package definition '$DefinitionId' definitionSignature.kind must be signed or unsigned." } if (-not [string]::Equals([string]$signature.format, $script:PackageDefinitionSignatureFormat, [System.StringComparison]::Ordinal)) { throw "Package definition '$DefinitionId' definitionSignature.format must be '$script:PackageDefinitionSignatureFormat'." } if (-not [string]::Equals([string]$signature.signedContent, $script:PackageDefinitionSignedContentKind, [System.StringComparison]::Ordinal)) { throw "Package definition '$DefinitionId' definitionSignature.signedContent must be '$script:PackageDefinitionSignedContentKind'." } if ([string]::Equals($kind, 'unsigned', [System.StringComparison]::OrdinalIgnoreCase)) { if ($signature.PSObject.Properties['signatureValue'] -and -not [string]::IsNullOrWhiteSpace([string]$signature.signatureValue)) { throw "Package definition '$DefinitionId' unsigned definitionSignature must not define signatureValue." } if ($signature.PSObject.Properties['certificatePem'] -and -not [string]::IsNullOrWhiteSpace([string]$signature.certificatePem)) { throw "Package definition '$DefinitionId' unsigned definitionSignature must not define certificatePem." } return } foreach ($requiredSignedProperty in @('keyThumbprint', 'signerDisplayName', 'signedAtUtc', 'signatureValue')) { if (-not $signature.PSObject.Properties[$requiredSignedProperty] -or [string]::IsNullOrWhiteSpace([string]$signature.$requiredSignedProperty)) { throw "Package definition '$DefinitionId' signed definitionSignature is missing '$requiredSignedProperty'." } } if (([string]$signature.keyThumbprint) -notmatch '^[A-Fa-f0-9]{40,128}$') { throw "Package definition '$DefinitionId' signed definitionSignature.keyThumbprint is not a hex thumbprint." } if ($signature.PSObject.Properties['certificatePem'] -and [string]::IsNullOrWhiteSpace([string]$signature.certificatePem)) { throw "Package definition '$DefinitionId' signed definitionSignature.certificatePem must not be empty." } try { [Convert]::FromBase64String([string]$signature.signatureValue) | Out-Null } catch { throw "Package definition '$DefinitionId' signed definitionSignature.signatureValue must be base64." } } function Assert-PackageDefinitionAcquisitionCandidateKind_1_8 { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DefinitionId, [Parameter(Mandatory = $true)] [psobject]$Candidate, [Parameter(Mandatory = $true)] [string]$CandidatePath ) $kind = if ($Candidate.PSObject.Properties['kind']) { [string]$Candidate.kind } else { $null } if ([string]::IsNullOrWhiteSpace($kind)) { throw "Package definition '$DefinitionId' schemaVersion 1.8 acquisition candidate '$CandidatePath' is missing kind." } switch -Exact ($kind) { 'packageDepot' { return } 'vendorDownload' { return } 'download' { throw "Package definition '$DefinitionId' schemaVersion 1.8 acquisition candidate '$CandidatePath' uses retired kind 'download'. Use 'vendorDownload'." } 'filesystem' { throw "Package definition '$DefinitionId' schemaVersion 1.8 acquisition candidate '$CandidatePath' uses retired package-definition kind 'filesystem'. Use packageDepot with PackageDepotInventory.json depot sources." } default { throw "Package definition '$DefinitionId' schemaVersion 1.8 acquisition candidate '$CandidatePath' uses unsupported kind '$kind'. Use packageDepot or vendorDownload." } } } function Assert-PackageDefinitionVendorDownloadCandidate_1_8 { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$Definition, [Parameter(Mandatory = $true)] [string]$DefinitionId, [Parameter(Mandatory = $true)] [psobject]$VersionEntry, [Parameter(Mandatory = $true)] [string]$TargetId, [Parameter(Mandatory = $true)] [psobject]$Artifact, [Parameter(Mandatory = $true)] [psobject]$Candidate ) if (-not [string]::Equals([string]$Candidate.kind, 'vendorDownload', [System.StringComparison]::OrdinalIgnoreCase)) { return } $releaseLabel = [string]$VersionEntry.version $hasSourceId = Test-PackageDefinitionTextPropertyPresent_1_8 -InputObject $Candidate -PropertyName 'sourceId' $hasCandidateSourcePath = Test-PackageDefinitionTextPropertyPresent_1_8 -InputObject $Candidate -PropertyName 'sourcePath' $hasArtifactSourcePath = Test-PackageDefinitionTextPropertyPresent_1_8 -InputObject $Artifact -PropertyName 'sourcePath' $hasCandidateUrl = Test-PackageDefinitionTextPropertyPresent_1_8 -InputObject $Candidate -PropertyName 'url' $hasCandidateUrlTemplate = Test-PackageDefinitionTextPropertyPresent_1_8 -InputObject $Candidate -PropertyName 'urlTemplate' $hasArtifactUrl = Test-PackageDefinitionTextPropertyPresent_1_8 -InputObject $Artifact -PropertyName 'url' $hasArtifactUrlTemplate = Test-PackageDefinitionTextPropertyPresent_1_8 -InputObject $Artifact -PropertyName 'urlTemplate' $directDownloadCount = 0 foreach ($hasDirectDownload in @($hasCandidateUrl, $hasCandidateUrlTemplate, $hasArtifactUrl, $hasArtifactUrlTemplate)) { if ($hasDirectDownload) { $directDownloadCount++ } } if ($directDownloadCount -gt 1) { throw "Package definition '$DefinitionId' release '$releaseLabel' artifact '$TargetId' vendorDownload candidate must define only one direct url/urlTemplate location." } if ($directDownloadCount -gt 0 -and ($hasSourceId -or $hasCandidateSourcePath -or $hasArtifactSourcePath)) { throw "Package definition '$DefinitionId' release '$releaseLabel' artifact '$TargetId' vendorDownload candidate must use either direct url/urlTemplate or sourceId with sourcePath, not both." } if ($directDownloadCount -gt 0) { return } if (-not $hasSourceId) { throw "Package definition '$DefinitionId' release '$releaseLabel' artifact '$TargetId' vendorDownload candidate requires sourceId, direct url, or urlTemplate." } if (-not (Test-PackageObjectHasProperty -InputObject $Definition.artifacts.sources -Name ([string]$Candidate.sourceId))) { throw "Package definition '$DefinitionId' release '$releaseLabel' artifact '$TargetId' references unknown artifacts source '$($Candidate.sourceId)'." } $releaseUpstream = if ($VersionEntry.PSObject.Properties['upstreamRelease']) { $VersionEntry.upstreamRelease } else { $null } $candidateSource = Get-PackageObjectPropertyValue -InputObject $Definition.artifacts.sources -Name ([string]$Candidate.sourceId) if ($candidateSource -and [string]::Equals([string]$candidateSource.kind, 'githubRelease', [System.StringComparison]::OrdinalIgnoreCase)) { if (-not $releaseUpstream -or -not $releaseUpstream.PSObject.Properties['sourceId'] -or [string]::IsNullOrWhiteSpace([string]$releaseUpstream.sourceId) -or -not [string]::Equals([string]$releaseUpstream.sourceId, [string]$Candidate.sourceId, [System.StringComparison]::OrdinalIgnoreCase) -or -not $releaseUpstream.PSObject.Properties['releaseTag'] -or [string]::IsNullOrWhiteSpace([string]$releaseUpstream.releaseTag)) { throw "Package definition '$DefinitionId' release '$releaseLabel' artifact '$TargetId' requires releaseTag because candidate '$($Candidate.sourceId)' uses GitHub release." } return } if (-not ($hasCandidateSourcePath -or $hasArtifactSourcePath)) { throw "Package definition '$DefinitionId' release '$releaseLabel' artifact '$TargetId' vendorDownload candidate requires sourcePath, artifact sourcePath, url, or urlTemplate." } } function Assert-PackageDefinitionAcquisitionVocabulary_1_8 { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$DefinitionDocumentInfo, [Parameter(Mandatory = $true)] [string]$DefinitionId ) $definition = $DefinitionDocumentInfo.Document $targetsById = @{} foreach ($target in @($definition.artifacts.targets)) { if ($target -and $target.PSObject.Properties['id']) { $targetsById[[string]$target.id] = $target } $candidateIndex = 0 $targetCandidates = if ($target -and $target.PSObject.Properties['acquisitionCandidates']) { @($target.acquisitionCandidates) } else { @() } foreach ($candidate in @($targetCandidates)) { Assert-PackageDefinitionAcquisitionCandidateKind_1_8 -DefinitionId $DefinitionId -Candidate $candidate -CandidatePath ("artifacts.targets['{0}'].acquisitionCandidates[{1}]" -f [string]$target.id, $candidateIndex) $candidateIndex++ } } foreach ($versionEntry in @($definition.artifacts.releases)) { foreach ($artifactProperty in @($versionEntry.targetArtifacts.PSObject.Properties)) { $targetId = [string]$artifactProperty.Name $artifact = $artifactProperty.Value $candidateIndex = 0 $declaredArtifactCandidates = if ($artifact -and $artifact.PSObject.Properties['acquisitionCandidates']) { @($artifact.acquisitionCandidates) } else { @() } foreach ($candidate in @($declaredArtifactCandidates)) { Assert-PackageDefinitionAcquisitionCandidateKind_1_8 -DefinitionId $DefinitionId -Candidate $candidate -CandidatePath ("artifacts.releases['{0}'].targetArtifacts['{1}'].acquisitionCandidates[{2}]" -f [string]$versionEntry.version, $targetId, $candidateIndex) $candidateIndex++ } $artifactAcquisitionCandidates = if ($artifact -and $artifact.PSObject.Properties['acquisitionCandidates']) { @($artifact.acquisitionCandidates) } else { @() } if (-not $artifactAcquisitionCandidates -and $targetsById.ContainsKey($targetId) -and $targetsById[$targetId].PSObject.Properties['acquisitionCandidates']) { $artifactAcquisitionCandidates = @($targetsById[$targetId].acquisitionCandidates) } foreach ($candidate in @($artifactAcquisitionCandidates)) { Assert-PackageDefinitionVendorDownloadCandidate_1_8 -Definition $definition -DefinitionId $DefinitionId -VersionEntry $versionEntry -TargetId $targetId -Artifact $artifact -Candidate $candidate } } } } function Assert-PackageDefinitionSchema { <# .SYNOPSIS Validates the Package definition schema for this package pass. .DESCRIPTION Rejects retired top-level names, requires schemaVersion 1.8 fields, then validates dependencies/artifacts/discovery/packageOperations references. .PARAMETER DefinitionDocumentInfo The loaded Package definition document info. .PARAMETER DefinitionId The expected definition id. .EXAMPLE Assert-PackageDefinitionSchema -DefinitionDocumentInfo $definitionInfo -DefinitionId VSCodeRuntime #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$DefinitionDocumentInfo, [Parameter(Mandatory = $true)] [string]$DefinitionId, [AllowNull()] [string]$PublisherId = $null ) $definition = $DefinitionDocumentInfo.Document foreach ($retiredProperty in @( 'classification', 'target', 'origins', 'interfaces', 'packageType', 'paths', 'sources', 'packages', 'entryPoints', 'packageFamily', 'managedPaths' )) { if ($definition.PSObject.Properties[$retiredProperty]) { throw "Package definition '$($DefinitionDocumentInfo.Path)' still uses retired property '$retiredProperty'." } } $schemaVersionText = [string]$definition.schemaVersion if ([string]::IsNullOrWhiteSpace($schemaVersionText)) { throw "Package definition '$($DefinitionDocumentInfo.Path)' defines schemaVersion, but it is empty." } Assert-PackageDefinitionSchemaVersionSupported -SchemaVersionText $schemaVersionText -DefinitionDocumentPath $DefinitionDocumentInfo.Path $retiredRootReplacements = @{ presenceDiscovery = 'discovery.presence' existingInstallDiscovery = 'discovery.existingInstall' } foreach ($retiredProperty in @($retiredRootReplacements.Keys)) { if ($definition.PSObject.Properties[$retiredProperty]) { throw "Package definition '$($DefinitionDocumentInfo.Path)' still uses retired root property '$retiredProperty'. Use '$($retiredRootReplacements[$retiredProperty])'." } } foreach ($requiredProperty in @('schemaVersion', 'definitionPublication', 'display', 'dependencies', 'artifacts', 'discovery', 'packageOperations')) { if (-not $definition.PSObject.Properties[$requiredProperty]) { throw "Package definition '$($DefinitionDocumentInfo.Path)' is missing required property '$requiredProperty'." } } foreach ($retiredProperty in @('definitionId', 'repositoryId')) { if ($definition.PSObject.Properties[$retiredProperty]) { throw "Package definition '$($DefinitionDocumentInfo.Path)' still uses retired schema 1.4 root property '$retiredProperty'. Move definition identity to definitionPublication." } } foreach ($retiredProperty in @( 'packageTargets', 'versionCatalog', 'upstreamSources', 'stateDiscovery', 'installedStateCheck', 'providedTools', 'releaseDefaults', 'existingInstallPolicy' )) { if ($definition.PSObject.Properties[$retiredProperty]) { throw "Package definition '$($DefinitionDocumentInfo.Path)' still uses retired pre-1.3 property '$retiredProperty'." } } switch -Exact ($schemaVersionText) { '1.8' { Assert-PackageDefinitionSchema_1_8 -DefinitionDocumentInfo $DefinitionDocumentInfo -DefinitionId $DefinitionId -PublisherId $PublisherId Assert-PackageDefinitionSignatureSchema_1_8 -DefinitionDocumentInfo $DefinitionDocumentInfo -DefinitionId $DefinitionId Assert-PackageDefinitionAcquisitionVocabulary_1_8 -DefinitionDocumentInfo $DefinitionDocumentInfo -DefinitionId $DefinitionId return } default { throw "Package definition '$($DefinitionDocumentInfo.Path)' encountered unsupported schemaVersion '$schemaVersionText' after validation gate." } } } |