import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { allowedInvalidEdges, npmAuditArgs, securedBundleDependencies, validateAuditPayload, validateNpmLsPayload, } from './taro-supply-chain-audit.js'; import { enforceTaroH5RuntimePatches, taroButtonLoadingPatch, taroH5RuntimePatchDefinition, taroInputWatcherPatch, } from './taro-components-h5-runtime-patch.js'; const repoRoot = process.cwd(); const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); const packageLock = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8')); const taroPackage = JSON.parse(fs.readFileSync(path.join(repoRoot, 'apps', 'taro', 'package.json'), 'utf8')); assert.equal(taroPackage.devDependencies?.['@tarojs/components'], taroH5RuntimePatchDefinition.packageVersion); assert.equal(taroPackage.scripts?.postinstall, taroH5RuntimePatchDefinition.postinstallCommand); assert.equal(packageLock.packages?.['apps/taro']?.hasInstallScript, true); const patchState = enforceTaroH5RuntimePatches({ root: repoRoot, mode: 'check' }); assert.equal(patchState.status, 'pass'); assert.equal(patchState.patches.inputWatcher.installedSha256, taroInputWatcherPatch.patchedSha256); assert.equal(patchState.patches.buttonLoading.installedSha256, taroButtonLoadingPatch.patchedSha256); for (const [name, expected] of Object.entries(securedBundleDependencies)) { assert.equal(packageJson.overrides?.[name], expected.version, `${name} override must stay exact`); assert.equal(packageLock.packages?.[`node_modules/${name}`]?.version, expected.version, `${name} lock version must stay exact`); assert.equal(packageLock.packages?.[`node_modules/${name}`]?.integrity, expected.integrity, `${name} lock integrity must stay exact`); } const dependencyNode = (name, parents) => ({ version: securedBundleDependencies[name].version, invalid: parents.map(parent => `"${parent.declared}" from node_modules/${parent.parent}`).join(', '), problems: [`invalid: ${name}@${securedBundleDependencies[name].version} /repo/node_modules/${name}`], }); const edgesFor = dependency => allowedInvalidEdges.filter(edge => edge.dependency === dependency); const validLs = { error: { code: 'ELSPROBLEMS' }, problems: Object.entries(securedBundleDependencies).map(([name, expected]) => `invalid: ${name}@${expected.version} /repo/node_modules/${name}`), dependencies: { '@tiku-saas/taro': { dependencies: { '@tarojs/components': { dependencies: { swiper: dependencyNode('swiper', edgesFor('swiper').filter(edge => edge.parent === '@tarojs/components')), }, }, '@tarojs/plugin-platform-h5': { dependencies: { '@tarojs/components-react': { dependencies: { swiper: dependencyNode('swiper', edgesFor('swiper').filter(edge => edge.parent === '@tarojs/components-react')), }, }, '@tarojs/taro-h5': { dependencies: { 'lodash-es': dependencyNode('lodash-es', edgesFor('lodash-es').filter(edge => edge.parent === '@tarojs/taro-h5')), }, }, 'lodash-es': dependencyNode('lodash-es', edgesFor('lodash-es').filter(edge => edge.parent === '@tarojs/plugin-platform-h5')), }, }, }, }, }, }; const lsSummary = validateNpmLsPayload(validLs); assert.equal(lsSummary.edges.length, 4); assert.throws( () => validateNpmLsPayload({ ...validLs, problems: [...validLs.problems, 'extraneous: unsafe@1.0.0 /repo/node_modules/unsafe'] }), /unexpected problem|unapproved problem/, 'new npm ls problems must fail closed', ); const missingEdgeLs = structuredClone(validLs); delete missingEdgeLs.dependencies['@tiku-saas/taro'].dependencies['@tarojs/plugin-platform-h5'].dependencies['@tarojs/components-react']; assert.throws(() => validateNpmLsPayload(missingEdgeLs), /once per reviewed invalid edge|invalid-edge set changed/, 'the exception set must not silently shrink or change'); const auditFixture = { vulnerabilities: { '@tarojs/cli': { severity: 'high', isDirect: true }, download: { severity: 'critical', isDirect: false, via: [] }, 'git-clone': { severity: 'high', isDirect: false, via: [{ source: 1093404, severity: 'high' }] }, esbuild: { severity: 'moderate', isDirect: false }, }, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 1, high: 2, critical: 1, total: 4 }, }, }; const auditSummary = validateAuditPayload(auditFixture); assert.equal(auditSummary.reviewedHighCritical.length, 3); assert.deepEqual(auditSummary.reviewedAdvisories, [1093404]); assert.throws( () => validateAuditPayload({ ...auditFixture, vulnerabilities: { ...auditFixture.vulnerabilities, swiper: { severity: 'critical', isDirect: false } }, }), /still reports swiper/, 'bundle dependency advisories must fail the gate', ); assert.throws( () => validateAuditPayload({ ...auditFixture, vulnerabilities: { ...auditFixture.vulnerabilities, 'new-build-risk': { severity: 'high', isDirect: false } }, }), /new unreviewed high/, 'new high or critical toolchain findings must require review', ); assert.throws( () => validateAuditPayload({ ...auditFixture, vulnerabilities: { ...auditFixture.vulnerabilities, 'git-clone': { severity: 'high', isDirect: false, via: [{ source: 9999999, severity: 'high' }] }, }, }), /new unreviewed high Taro advisory/, 'new advisories on an already allowlisted package must require review', ); const auditArgs = npmAuditArgs('https://registry.npmjs.org/'); assert.ok(auditArgs.includes('--workspace')); assert.ok(auditArgs.includes('@tiku-saas/taro')); assert.equal(auditArgs.some(arg => arg === '--omit=dev' || arg.startsWith('--omit=')), false, 'Taro audit must include dependencies that are marked dev but bundled into H5'); console.log('[PASS] Taro supply-chain audit contract');