[CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet('purchase', 'leave')] [string]$Workflow, [Parameter(Mandatory = $true)] [ValidatePattern('^[A-Za-z0-9_.:-]{1,64}$')] [string]$ModuleCode, [Parameter(Mandatory = $true)][string]$AccountBook, [Parameter(Mandatory = $true)][string]$SubSystemId, [Parameter(Mandatory = $true)][ValidatePattern('^[a-z0-9_.-]{1,128}$')][string]$AdapterId, [Parameter(Mandatory = $true)][ValidatePattern('^[a-z0-9_.-]{1,128}$')][string]$AdapterVersion, [Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{8,128}$')][string]$EvidenceId, [Parameter(Mandatory = $true)][string]$RuntimeConfigurationFile, [Parameter(Mandatory = $true)][string]$CustomerProfileFile, [Parameter(Mandatory = $true)][string]$FieldMappingEvidence, [Parameter(Mandatory = $true)][string]$ReadContractEvidence, [Parameter(Mandatory = $true)][string]$WriteIntegrationEvidence, [Parameter(Mandatory = $true)][string]$VerifierCliPath, [Parameter(Mandatory = $true)][ValidateLength(1, 128)][string]$ErpUser, [Parameter(Mandatory = $true)][System.Security.SecureString]$ErpPassword, [Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{40}$')][string]$ExpectedSourceCommit, [Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$ExpectedPackageSha256, [Parameter(Mandatory = $true)][string]$ValidatedBy, [Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9 ]{40,59}$')][string]$CertificateThumbprint, [Parameter(Mandatory = $true)][string]$OutputPath, [ValidateRange(1, 366)][int]$ValidDays = 90 ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' function Get-Sha256Hex([byte[]]$Bytes) { $sha = [System.Security.Cryptography.SHA256]::Create() try { $hash = $sha.ComputeHash($Bytes) return ([System.BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } } function Get-FileSha256( [string]$Path, [long]$MaximumBytes, [string]$Label) { $full = [System.IO.Path]::GetFullPath($Path) if (-not [System.IO.File]::Exists($full)) { throw "$Label file not found: $full" } $info = New-Object System.IO.FileInfo($full) if ($info.Length -le 0 -or $info.Length -gt $MaximumBytes -or (($info.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) { throw "$Label must be a non-empty regular file no larger than $MaximumBytes bytes: $full" } $bytes = [System.IO.File]::ReadAllBytes($full) if ($bytes.Length -le 0 -or $bytes.Length -gt $MaximumBytes) { throw "$Label changed size while it was read: $full" } return Get-Sha256Hex $bytes } function Open-InputLock( [string]$Path, [long]$MaximumBytes, [string]$Label) { $full = [System.IO.Path]::GetFullPath($Path) if (-not [System.IO.File]::Exists($full)) { throw "$Label file not found: $full" } $info = New-Object System.IO.FileInfo($full) if ($info.Length -le 0 -or $info.Length -gt $MaximumBytes -or (($info.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) { throw "$Label must be a non-empty ordinary file within the size limit: $full" } $stream = [System.IO.File]::Open( $full, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read) if ($stream.Length -le 0 -or $stream.Length -gt $MaximumBytes) { $stream.Dispose() throw "$Label changed size while it was locked: $full" } return $stream } function Find-SigningCertificate([string]$Thumbprint) { $normalized = ($Thumbprint -replace '\s+', '').ToUpperInvariant() foreach ($location in @('CurrentUser', 'LocalMachine')) { $path = "Cert:\$location\TrustedPeople\$normalized" if (Test-Path -LiteralPath $path) { $certificate = Get-Item -LiteralPath $path if (-not $certificate.HasPrivateKey) { throw "TrustedPeople certificate has no private key: $normalized" } if ((Get-Date) -lt $certificate.NotBefore -or (Get-Date) -gt $certificate.NotAfter) { throw "TrustedPeople certificate is not currently valid: $normalized" } return $certificate } } throw "Certificate not found in CurrentUser/LocalMachine TrustedPeople: $normalized" } function Test-ExactProperties([object]$Value, [string[]]$Expected) { if ($null -eq $Value) { return $false } $names = @($Value.PSObject.Properties | ForEach-Object { $_.Name }) if ($names.Count -ne $Expected.Count) { return $false } foreach ($name in $Expected) { if ($names -cnotcontains $name) { return $false } } return $true } function Invoke-AuthenticatedCli( [string]$CliPath, [string[]]$Arguments, [string]$User, [System.Security.SecureString]$Password, [string]$Ledger, [string]$Subsystem) { $pointer = [IntPtr]::Zero $plainText = $null try { $pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) $plainText = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) $allArguments = @($Arguments) + @( '--user', $User, '--password-stdin', '--ledger', $Ledger, '--subsystem', $Subsystem ) $output = @($plainText | & $CliPath @allArguments 2>&1) $exitCode = $LASTEXITCODE return [pscustomobject]@{ ExitCode = $exitCode Text = (($output | ForEach-Object { [string]$_ }) -join ` [Environment]::NewLine) } } finally { $plainText = $null if ($pointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } } } function Invoke-FieldMappingVerifier( [string]$CliPath, [string]$MappingPath, [string]$ExpectedWorkflow, [string]$ExpectedModule, [string]$ExpectedAccountBook, [string]$ExpectedSubsystem, [string]$User, [System.Security.SecureString]$Password) { $result = Invoke-AuthenticatedCli ` $CliPath ` @('adapters', 'validate-fields', $ExpectedWorkflow, '--input', [System.IO.Path]::GetFullPath($MappingPath)) ` $User $Password $ExpectedAccountBook $ExpectedSubsystem if ($result.ExitCode -ne 0) { throw 'Field mapping failed live ERP metadata validation.' } try { $response = $result.Text | ConvertFrom-Json } catch { throw 'Field mapping verifier did not return valid JSON.' } $expectedData = @( 'workflow', 'moduleCode', 'moduleKind', 'erpScope', 'fieldMapReady', 'registrationReady', 'issues', 'requiredRuntimeEvidence', 'storage', 'note' ) if (-not (Test-ExactProperties $response @('ok', 'correlationId', 'data')) -or $response.ok -ne $true -or -not (Test-ExactProperties $response.data $expectedData) -or -not (Test-ExactProperties $response.data.erpScope ` @('accountBook', 'subSystemId', 'validatedByUserId')) -or $response.data.workflow -cne $ExpectedWorkflow -or $response.data.moduleCode -cne $ExpectedModule -or $response.data.erpScope.accountBook -ne $ExpectedAccountBook -or $response.data.erpScope.subSystemId -ne $ExpectedSubsystem -or [string]::IsNullOrWhiteSpace( [string]$response.data.erpScope.validatedByUserId) -or $response.data.fieldMapReady -ne $true -or @($response.data.issues).Count -ne 0 -or $response.data.registrationReady -ne $false) { throw 'Field mapping verifier response is not ready or not bound to this ERP scope.' } return $response.data } function Invoke-ReadContractEvidenceVerifier( [string]$CliPath, [string]$EvidencePath, [string]$ExpectedWorkflow, [string]$ExpectedModule, [string]$ExpectedAccountBook, [string]$ExpectedSubsystem) { $output = @(& $CliPath ` 'adapters' 'verify-contract-evidence' ` '--input' ([System.IO.Path]::GetFullPath($EvidencePath)) ` '--workflow' $ExpectedWorkflow ` '--module' $ExpectedModule ` '--account-book' $ExpectedAccountBook ` '--subsystem' $ExpectedSubsystem 2>&1) $exitCode = $LASTEXITCODE $text = (($output | ForEach-Object { [string]$_ }) -join [Environment]::NewLine) if ($exitCode -ne 0) { throw 'Read contract evidence failed strict CLI verification.' } try { $response = $text | ConvertFrom-Json } catch { throw 'Read contract verifier did not return valid JSON.' } $expectedData = @( 'packageType', 'schemaVersion', 'workflow', 'moduleCode', 'contentSha256', 'integrityValid', 'verified', 'signatureVerified', 'registrationReady', 'note', 'scopeBindingVerified' ) if (-not (Test-ExactProperties $response @('ok', 'correlationId', 'data')) -or $response.ok -ne $true -or -not (Test-ExactProperties $response.data $expectedData) -or $response.data.packageType -ne 'workflow_read_contract_evidence' -or $response.data.schemaVersion -ne '1.0' -or $response.data.workflow -cne $ExpectedWorkflow -or $response.data.moduleCode -cne $ExpectedModule -or $response.data.integrityValid -ne $true -or $response.data.verified -ne $true -or $response.data.scopeBindingVerified -ne $true -or $response.data.signatureVerified -ne $false -or $response.data.registrationReady -ne $false) { throw 'Read contract evidence is not verified or not bound to this acceptance scope.' } return $response.data } function Invoke-WriteEvidenceVerifier( [string]$CliPath, [string]$EvidencePath, [string]$RuntimeHash) { $cli = [System.IO.Path]::GetFullPath($CliPath) if (-not [System.IO.File]::Exists($cli)) { throw 'Verifier CLI does not exist.' } $info = New-Object System.IO.FileInfo($cli) if ($info.Length -le 0 -or $info.Length -gt 64MB -or (($info.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) -or [System.IO.Path]::GetFileName($cli) -ne 'lserp-cli.exe') { throw 'Verifier CLI must be a non-empty ordinary lserp-cli.exe file no larger than 64 MB.' } $arguments = @( 'adapters', 'verify-write-integration-evidence', '--input', [System.IO.Path]::GetFullPath($EvidencePath), '--workflow', $Workflow, '--module', $ModuleCode, '--account-book', $AccountBook, '--subsystem', $SubSystemId, '--runtime-sha256', $RuntimeHash, '--source-commit', $ExpectedSourceCommit.ToLowerInvariant(), '--package-sha256', $ExpectedPackageSha256.ToLowerInvariant() ) $output = @(& $cli @arguments 2>&1) $exitCode = $LASTEXITCODE $text = (($output | ForEach-Object { [string]$_ }) -join [Environment]::NewLine) if ($exitCode -ne 0) { throw 'Write integration evidence failed strict CLI verification.' } try { $response = $text | ConvertFrom-Json } catch { throw 'Verifier CLI did not return valid JSON.' } $expectedData = @( 'evidenceType', 'schemaVersion', 'contentSha256', 'workflow', 'moduleCode', 'erpScope', 'sourceCommit', 'packageSha256', 'runtimeConfigurationSha256', 'uatAuthorizationSourceSha256', 'uatAuthorizationContentSha256', 'uatAuthorizationIdSha256', 'environmentId', 'testedAtUtc', 'testedBy', 'caseCount', 'verified', 'registrationReady' ) if (-not (Test-ExactProperties $response @('ok', 'correlationId', 'data')) -or $response.ok -ne $true -or -not (Test-ExactProperties $response.data $expectedData) -or -not (Test-ExactProperties $response.data.erpScope @( 'accountBook', 'subSystemId', 'userIdSha256', 'userNameSha256', 'databaseScopeFingerprint', 'isAdministrator')) -or $response.data.evidenceType -ne 'workflow_write_integration' -or $response.data.schemaVersion -ne '1.5' -or $response.data.verified -ne $true -or $response.data.registrationReady -ne $false -or $response.data.workflow -ne $Workflow -or $response.data.moduleCode -ne $ModuleCode -or $response.data.erpScope.accountBook -ne $AccountBook -or $response.data.erpScope.subSystemId -ne $SubSystemId -or $response.data.runtimeConfigurationSha256 -ne $RuntimeHash.ToLowerInvariant() -or $response.data.sourceCommit -ne $ExpectedSourceCommit.ToLowerInvariant() -or $response.data.packageSha256 -ne $ExpectedPackageSha256.ToLowerInvariant() -or [int]$response.data.caseCount -ne $(if ($Workflow -eq 'purchase') { 13 } else { 19 })) { throw 'Verifier CLI response is not bound to this acceptance scope and artifact.' } return $response.data } function Invoke-CustomerProfileVerifier( [string]$CliPath, [string]$ProfilePath, [string]$ExpectedHash, [string]$ExpectedWorkflow, [string]$ExpectedAccountBook, [string]$ExpectedSubsystem, [string]$User, [System.Security.SecureString]$Password) { $cli = [System.IO.Path]::GetFullPath($CliPath) $profile = [System.IO.Path]::GetFullPath($ProfilePath) $result = Invoke-AuthenticatedCli ` $cli ` @('adapters', 'revalidate-profile', '--input', $profile) ` $User $Password $ExpectedAccountBook $ExpectedSubsystem if ($result.ExitCode -notin @(0, 6)) { throw 'Customer profile failed online catalog revalidation.' } try { $response = $result.Text | ConvertFrom-Json } catch { throw 'Customer profile verifier did not return valid JSON.' } $expectedData = @( 'schemaVersion', 'profileType', 'profileSha256', 'profileSafetyValidated', 'metadataQueryScope', 'onlineMetadataMatches', 'criticalCatalogContractMatches', 'driftCodes', 'openActivationBlockerCount', 'workflowActivation', 'activationAllowed', 'registrationReady', 'note' ) $allowedNonCriticalDriftCodes = @( 'profile_table_count_changed', 'profile_view_count_changed', 'profile_procedure_count_changed', 'profile_trigger_count_changed', 'profile_agent_object_state_changed' ) $unexpectedDriftCodes = @($response.data.driftCodes | Where-Object { [string]$_ -notin $allowedNonCriticalDriftCodes }) try { $strictUtf8 = New-Object System.Text.UTF8Encoding($false, $true) $profileDocument = [System.IO.File]::ReadAllText($profile, $strictUtf8) | ConvertFrom-Json } catch { throw 'Customer profile cannot be reread as strict UTF-8 JSON.' } if (@($response.data.driftCodes) -contains 'profile_agent_object_state_changed' -and $profileDocument.database.agentWorkflowObjectsPresent -ne $false) { $unexpectedDriftCodes += 'profile_agent_object_state_changed' } $selectedActivation = if ($ExpectedWorkflow -eq 'purchase') { $response.data.workflowActivation.purchase } else { $response.data.workflowActivation.leave } if (-not (Test-ExactProperties $response @('ok', 'correlationId', 'data')) -or $response.ok -ne $true -or -not (Test-ExactProperties $response.data $expectedData) -or -not (Test-ExactProperties $response.data.workflowActivation ` @('purchase', 'leave')) -or -not (Test-ExactProperties $response.data.workflowActivation.purchase ` @('approved', 'openBlockerCount')) -or -not (Test-ExactProperties $response.data.workflowActivation.leave ` @('approved', 'openBlockerCount')) -or $response.data.schemaVersion -ne '1.2' -or $response.data.profileType -ne 'readonly_low_code_metadata_review' -or $response.data.profileSha256 -cne $ExpectedHash -or $response.data.profileSafetyValidated -ne $true -or $response.data.metadataQueryScope -ne 'system_catalog_only' -or $response.data.criticalCatalogContractMatches -ne $true -or $unexpectedDriftCodes.Count -ne 0 -or $selectedActivation.approved -ne $true -or [int]$selectedActivation.openBlockerCount -ne 0 -or $response.data.activationAllowed -ne $false -or $response.data.registrationReady -ne $false) { throw 'Customer profile verifier response has unsafe identity, compatibility, or critical catalog drift.' } return $response.data } function Assert-ProfileResolutionBindings( [string]$ProfilePath, [string]$ExpectedWorkflow, [string]$ExpectedModule, [string]$FieldMappingHash, [string]$ReadContractHash, [string]$WriteIntegrationHash) { try { $strictUtf8 = New-Object System.Text.UTF8Encoding($false, $true) $profile = [System.IO.File]::ReadAllText( [System.IO.Path]::GetFullPath($ProfilePath), $strictUtf8) | ConvertFrom-Json } catch { throw 'Customer profile resolution bindings cannot be read as strict UTF-8 JSON.' } if ($profile.schemaVersion -cne '1.2') { throw 'Customer profile resolution bindings require schemaVersion 1.2.' } if ($ExpectedWorkflow -ceq 'purchase') { $profileModule = [string]$profile.purchaseTargetSelection.selectedModuleCode $blockers = @($profile.purchaseActivationBlockers) $expectedCodes = @( 'purchase_currency_field_not_configured', 'purchase_currency_crosswalk_not_approved', 'purchase_row_scope_not_approved', 'purchase_compat100_write_contract_not_approved', 'purchase_windows_integration_not_verified' ) } else { $profileModule = [string]$profile.modules.leave.moduleCode $blockers = @($profile.leaveActivationBlockers) $expectedCodes = @( 'leave_flow_type_rules_stale', 'leave_agent_schema_not_deployed', 'leave_compat100_write_contract_not_approved', 'leave_windows_integration_not_verified' ) } if ($profileModule -cne $ExpectedModule) { throw 'Customer profile workflow module is not bound to the acceptance module.' } if ($blockers.Count -ne $expectedCodes.Count) { throw 'Customer profile blocker resolution set is incomplete or contains unexpected entries.' } $seenCodes = @() foreach ($blocker in $blockers) { if (-not (Test-ExactProperties $blocker ` @('code', 'status', 'resolution', 'evidence'))) { throw 'Customer profile blocker resolution has an invalid fixed contract.' } $code = [string]$blocker.code if ($expectedCodes -cnotcontains $code -or $seenCodes -ccontains $code -or [string]$blocker.status -cne 'resolved' -or -not (Test-ExactProperties $blocker.resolution ` @('evidenceArtifact', 'evidenceSha256', 'approvedBy', 'approvedAtUtc'))) { throw 'Customer profile blocker resolution set or status is invalid.' } $seenCodes += $code $expectedArtifact = if ($code -ceq 'purchase_currency_field_not_configured') { 'field_mapping' } else { 'write_integration' } $artifact = [string]$blocker.resolution.evidenceArtifact $expectedHash = $null if ($artifact -ceq 'field_mapping') { $expectedHash = $FieldMappingHash } elseif ($artifact -ceq 'read_contract') { $expectedHash = $ReadContractHash } elseif ($artifact -ceq 'write_integration') { $expectedHash = $WriteIntegrationHash } $approvedAt = [DateTime]::MinValue $approvedAtValue = $blocker.resolution.approvedAtUtc if ($approvedAtValue -is [DateTime]) { # PowerShell 7 ConvertFrom-Json parses ISO timestamps eagerly. The # locked profile has already passed the CLI's raw exact-format gate. $approvedAt = [DateTime]$approvedAtValue $approvedAtValid = $approvedAt.Kind -eq [DateTimeKind]::Utc } else { $approvedAtValid = [DateTime]::TryParseExact( [string]$approvedAtValue, 'o', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind, [ref]$approvedAt) } if ($artifact -cne $expectedArtifact) { throw 'Customer profile blocker resolution uses the wrong evidence artifact kind.' } if ([string]::IsNullOrWhiteSpace([string]$expectedHash) -or [string]$blocker.resolution.evidenceSha256 -cne $expectedHash) { throw 'Customer profile blocker resolution is not bound to the exact signed evidence artifact.' } if ([string]::IsNullOrWhiteSpace([string]$blocker.resolution.approvedBy)) { throw 'Customer profile blocker resolution has no approving identity.' } if (-not $approvedAtValid -or $approvedAt.Kind -ne [DateTimeKind]::Utc) { throw 'Customer profile blocker resolution approval time is not exact UTC round-trip format.' } } return [pscustomobject]@{ Verified = $true ResolvedBlockerCount = $seenCodes.Count } } $inputDefinitions = @( @($VerifierCliPath, 64MB, 'Verifier CLI'), @($RuntimeConfigurationFile, 64KB, 'Runtime configuration'), @($CustomerProfileFile, 1MB, 'Customer profile'), @($FieldMappingEvidence, 1MB, 'Field mapping'), @($ReadContractEvidence, 4MB, 'Read contract evidence'), @($WriteIntegrationEvidence, 4MB, 'Write integration evidence') ) $inputPaths = @($inputDefinitions | ForEach-Object { [System.IO.Path]::GetFullPath([string]$_[0]) }) $fullOutputCandidate = [System.IO.Path]::GetFullPath($OutputPath) if (@($inputPaths | Sort-Object -Unique).Count -ne $inputPaths.Count -or $inputPaths -contains $fullOutputCandidate) { throw 'Verifier, evidence inputs and output must all use distinct files.' } $inputLocks = New-Object 'System.Collections.Generic.List[System.IDisposable]' try { foreach ($definition in $inputDefinitions) { $inputLocks.Add((Open-InputLock ` ([string]$definition[0]) ` ([long]$definition[1]) ` ([string]$definition[2]))) } } catch { foreach ($lock in $inputLocks) { $lock.Dispose() } throw } try { $issuedAt = [DateTime]::UtcNow $expiresAt = $issuedAt.AddDays($ValidDays) $thumbprint = ($CertificateThumbprint -replace '\s+', '').ToUpperInvariant() $runtimeConfigurationHash = Get-FileSha256 ` $RuntimeConfigurationFile 64KB 'Runtime configuration' $customerProfileHash = Get-FileSha256 ` $CustomerProfileFile 1MB 'Customer profile' $profileVerification = Invoke-CustomerProfileVerifier ` $VerifierCliPath $CustomerProfileFile $customerProfileHash $Workflow ` $AccountBook $SubSystemId $ErpUser $ErpPassword if ((Get-FileSha256 $CustomerProfileFile 1MB 'Customer profile') -ne $customerProfileHash) { throw 'Customer profile changed while it was being verified.' } $fieldMappingHash = Get-FileSha256 ` $FieldMappingEvidence 1MB 'Field mapping' $fieldMappingVerification = Invoke-FieldMappingVerifier ` $VerifierCliPath $FieldMappingEvidence $Workflow $ModuleCode ` $AccountBook $SubSystemId $ErpUser $ErpPassword if ((Get-FileSha256 $FieldMappingEvidence 1MB 'Field mapping') -ne $fieldMappingHash) { throw 'Field mapping changed while it was being verified.' } $readContractHash = Get-FileSha256 ` $ReadContractEvidence 4MB 'Read contract evidence' $readContractVerification = Invoke-ReadContractEvidenceVerifier ` $VerifierCliPath $ReadContractEvidence $Workflow $ModuleCode ` $AccountBook $SubSystemId if ((Get-FileSha256 $ReadContractEvidence 4MB 'Read contract evidence') -ne $readContractHash) { throw 'Read contract evidence changed while it was being verified.' } $writeIntegrationHash = Get-FileSha256 ` $WriteIntegrationEvidence 4MB 'Write integration evidence' $writeVerification = Invoke-WriteEvidenceVerifier ` $VerifierCliPath $WriteIntegrationEvidence $runtimeConfigurationHash if ((Get-FileSha256 $WriteIntegrationEvidence 4MB 'Write integration evidence') -ne $writeIntegrationHash) { throw 'Write integration evidence changed while it was being verified.' } $profileResolutionVerification = Assert-ProfileResolutionBindings ` $CustomerProfileFile $Workflow $ModuleCode $fieldMappingHash ` $readContractHash $writeIntegrationHash $content = [ordered]@{ packageType = 'workflow_write_acceptance_evidence' workflow = $Workflow moduleCode = $ModuleCode erpScope = [ordered]@{ accountBook = $AccountBook subSystemId = $SubSystemId } adapterId = $AdapterId adapterVersion = $AdapterVersion evidenceId = $EvidenceId runtimeConfigurationSha256 = $runtimeConfigurationHash customerProfileSha256 = $customerProfileHash fieldMappingSha256 = $fieldMappingHash readContractEvidenceSha256 = $readContractHash writeIntegrationEvidenceSha256 = $writeIntegrationHash requirements = [ordered]@{ customerConfigurationValidated = $true parameterizedReadQueriesVerified = $true transactionalWriteVerified = $true persistentIdempotencyVerified = $true permissionRecheckVerified = $true windowsIntegrationVerified = $true criticalCatalogRuntimeRecheckVerified = $true } issuedAtUtc = $issuedAt.ToString('o') expiresAtUtc = $expiresAt.ToString('o') validatedBy = $ValidatedBy note = '客户 Windows 验收完成;当前工作流画像阻断项已关闭,运行时配置、客户画像在线目录复核和三个输入证据文件的 SHA-256 已绑定。' } $canonicalContent = $content | ConvertTo-Json -Compress -Depth 10 $utf8 = New-Object System.Text.UTF8Encoding($false, $true) $contentBytes = $utf8.GetBytes($canonicalContent) $contentHash = Get-Sha256Hex $contentBytes $certificate = Find-SigningCertificate $thumbprint $rsa = $certificate.PrivateKey -as [System.Security.Cryptography.RSACryptoServiceProvider] if ($null -eq $rsa) { throw 'Signing certificate must expose an RSA CSP private key for the .NET Framework 4.0 client.' } $sha = [System.Security.Cryptography.SHA256]::Create() try { $digest = $sha.ComputeHash($contentBytes) } finally { $sha.Dispose() } $signature = $rsa.SignHash( $digest, [System.Security.Cryptography.CryptoConfig]::MapNameToOID('SHA256')) $package = [ordered]@{ schemaVersion = '1.1' contentSha256 = $contentHash signatureAlgorithm = 'rsa-sha256' certificateThumbprint = $thumbprint signatureBase64 = [Convert]::ToBase64String($signature) content = $content } $body = $utf8.GetBytes(($package | ConvertTo-Json -Depth 10)) $fullOutput = [System.IO.Path]::GetFullPath($OutputPath) $directory = [System.IO.Path]::GetDirectoryName($fullOutput) if ([string]::IsNullOrWhiteSpace($directory) -or -not [System.IO.Directory]::Exists($directory)) { throw "Output directory does not exist: $directory" } $stream = [System.IO.File]::Open( $fullOutput, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None) try { $stream.Write($body, 0, $body.Length) $stream.Flush() } finally { $stream.Dispose() } [ordered]@{ outputFile = $fullOutput workflow = $Workflow moduleCode = $ModuleCode accountBook = $AccountBook subSystemId = $SubSystemId evidenceId = $EvidenceId evidenceSha256 = $contentHash runtimeConfigurationSha256 = $content.runtimeConfigurationSha256 customerProfileSha256 = $content.customerProfileSha256 customerProfileOnlineMetadataMatches = $profileVerification.onlineMetadataMatches customerProfileCriticalCatalogMatches = ` $profileVerification.criticalCatalogContractMatches customerProfileResolutionBindingVerified = ` $profileResolutionVerification.Verified customerProfileResolvedBlockerCount = ` $profileResolutionVerification.ResolvedBlockerCount fieldMapReady = $fieldMappingVerification.fieldMapReady readContractVerified = $readContractVerification.verified validatedAtUtc = $issuedAt.ToString('o') expiresAtUtc = $expiresAt.ToString('o') certificateThumbprint = $thumbprint sourceCommit = $writeVerification.sourceCommit packageSha256 = $writeVerification.packageSha256 writeIntegrationCaseCount = $writeVerification.caseCount nextStep = 'Use these exact values when inserting the V2 readiness row, then run lserp-cli adapters verify-acceptance-evidence.' } | ConvertTo-Json -Depth 5 } finally { foreach ($lock in $inputLocks) { $lock.Dispose() } }