$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot function Read-ProjectFile { param([string]$Path) $fullPath = Join-Path $root $Path if (-not (Test-Path -LiteralPath $fullPath)) { throw "缺少 TAC 集成文件:$Path" } return Get-Content -LiteralPath $fullPath -Raw -Encoding UTF8 } function Require-Text { param([string]$Content, [string]$Text, [string]$Label) if (-not $Content.Contains($Text)) { throw "$Label 缺少:$Text" } } function Reject-Text { param([string]$Content, [string]$Text, [string]$Label) if ($Content.Contains($Text)) { throw "$Label 仍保留:$Text" } } $owner = Read-ProjectFile 'utils/auth-verification.js' $adapter = Read-ProjectFile 'static/tac/js/jiapu-tac-adapter.js' $component = Read-ProjectFile 'components/TacVerification.vue' $api = Read-ProjectFile 'utils/api.js' $a01 = Read-ProjectFile 'pages/auth/a01-entry.vue' $a04 = Read-ProjectFile 'pages/auth/a04-register.vue' $a05 = Read-ProjectFile 'pages/auth/a05-reset-password.vue' $vendorAssets = [ordered]@{ 'static/tac/css/tac.css' = '181694518971a9f991d551b6a6e6dab2bf750f940bfc1673a158213f92eedbe0' 'static/tac/js/tac.min.js' = '505f73c051908d7b805db458990790be3e91f792c4001cec0ea9377d7d302b55' 'static/tac/images/icon.png' = '53e37ffc5bb81c46e6306b7d61d2eaa3de57e47ca6cdb8d5210022ae815c21c2' 'static/tac/images/dun.jpeg' = 'd9178a8c4cca36e3df6c3acd7e895ce9d34dd60ef3f1cf4a70c94d4324ed96e7' } foreach ($entry in $vendorAssets.GetEnumerator()) { $absolutePath = Join-Path $root $entry.Key if (-not (Test-Path -LiteralPath $absolutePath)) { throw "缺少用户提供的 TAC 资产:$($entry.Key)" } $actualHash = (Get-FileHash -LiteralPath $absolutePath -Algorithm SHA256).Hash.ToLowerInvariant() if ($actualHash -ne $entry.Value) { throw "用户提供的 TAC 供应商资产发生漂移:$($entry.Key)" } } foreach ($token in @('lang="renderjs"', './static/tac/css/tac.css', './static/tac/js/tac.min.js', './static/tac/js/jiapu-tac-adapter.js', 'window.TAC', 'window.CaptchaConfig', 'window.JiapuTacAdapter', 'xhr.status >= 200 && xhr.status < 300', '$ownerInstance.callMethod', 'activeXhr', 'xhr.timeout = 15000', 'xhr.ontimeout', 'xhr.abort()', 'config.doSendRequest = (options) => this.sendStrictRequest(options)', '@media (max-width: 340px)')) { Require-Text -Content $component -Text $token -Label 'TacVerification' } Reject-Text -Content $component -Text 'config.doSendRequest = this.sendStrictRequest' -Label '失去 renderjs 实例上下文的传输函数' $staleGuard = 'if (generation !== this.generation || !this.context || this.context.visible !== true) return;' if ([regex]::Matches($component, [regex]::Escape($staleGuard)).Count -lt 2) { throw 'TacVerification 必须在资源加载成功与失败两条分支都拒绝过期代次' } Require-Text -Content $adapter -Text 'payload: { track:' -Label 'TAC payload.track 适配器' foreach ($unsafe in @('code === 200 && response.data', 'passed !== false', "validToken: 'mock", 'mock-valid-token')) { Reject-Text -Content ($owner + $adapter + $component + $api) -Text $unsafe -Label 'TAC 安全合同' } foreach ($method in @('getCaptchaRequirement', 'sendSmsCode', 'loginWithPassword', 'loginWithSms', 'registerWithPassword', 'resetPassword')) { Require-Text -Content $api -Text "async $method" -Label '认证 API' } $authApiMatch = [regex]::Match($api, '(?s)async getCaptchaRequirement.*?(?=\s+async getProfile)') if (-not $authApiMatch.Success) { throw '无法定位唯一认证 API 区段' } Reject-Text -Content $authApiMatch.Value -Text "return { success: true }" -Label '认证短信 mock' Reject-Text -Content $authApiMatch.Value -Text "mock-session-token" -Label '认证会话 mock' Require-Text -Content $api -Text 'sceneCode, phone, validToken' -Label '短信票据请求' foreach ($token in @('const requestAuth =', 'strictEnvelope: true', 'expectedStatus: 200', "hasOwnProperty.call(data, 'data')")) { Require-Text -Content $api -Text $token -Label '认证严格响应 owner' } foreach ($token in @('const REQUEST_TIMEOUT_MS = 15000', 'export const createRequestController', "error?.code === 'REQUEST_CANCELLED'", 'task?.abort?.()')) { Require-Text -Content $api -Text $token -Label '认证请求生命周期 owner' } foreach ($page in @($a01, $a04, $a05)) { Require-Text -Content $page -Text '', 'if (blockBusyAction()) return;', 'const prepareForgotPassword = () => {', 'const prepareRegister = () => {')) { Require-Text -Content $a01 -Text $token -Label 'A01 忙碌动作门禁' } foreach ($token in @('AUTH_TAC_SCENE.SMS_LOGIN', 'appApi.loginWithPassword', 'appApi.loginWithSms', 'calcMD5(password.value)', 'const preparePasswordLogin = async () =>', '/^\d{4}$/')) { Require-Text -Content $a01 -Text $token -Label 'A01' } Reject-Text -Content $a01 -Text 'PASSWORD_TAC_BLOCKED_MESSAGE' -Label 'A01 旧密码登录硬关闭' Reject-Text -Content $a01 -Text '/^\d{6}$/' -Label 'A01 六位短信码' foreach ($token in @('AUTH_TAC_SCENE.REGISTER', 'v-model.trim="verificationCode"', 'appApi.registerWithPassword', 'calcMD5(password.value)', '/^\d{4}$/', 'goRoot("G01")')) { Require-Text -Content $a04 -Text $token -Label 'A04' } foreach ($token in @('AUTH_TAC_SCENE.FORGOT_PASSWORD', 'appApi.resetPassword', 'calcMD5(password.value)', '/^\d{4}$/', 'await appApi.resetPassword')) { Require-Text -Content $a05 -Text $token -Label 'A05' } Reject-Text -Content $a05 -Text '/^\d{6}$/' -Label 'A05 六位短信码' Write-Output 'AUTH-TAC-INTEGRATION-CONTRACT PASS'