Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/Deployment/New-WorkflowAcceptanceEvidence.ps1
2026-08-14 14:28:28 +08:00

676 lines
28 KiB
PowerShell

[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() }
}