import { createPublicKey } from "node:crypto"; import { readFile } from "node:fs/promises"; import { evaluatePromotionEvidence, type ProviderTrust, } from "./provider-evidence.ts"; import { RELEASE_CANDIDATE_MANIFEST_PATH, releaseCandidateManifestSchema, verifyReleaseCandidate, } from "./release-candidate.ts"; export async function verifyPromotionInputs( environment: NodeJS.ProcessEnv = process.env, ) { const manifestDocument = await requiredJson(RELEASE_CANDIDATE_MANIFEST_PATH); const manifest = releaseCandidateManifestSchema.parse(manifestDocument); const candidate = await verifyReleaseCandidate(manifestDocument); const localVerification = await requiredJson( "artifacts/security/supply-chain-verification.json", ); const vulnerabilityReport = await optionalJson( environment.VULNERABILITY_REPORT_PATH, ); const provenanceAttestation = await optionalJson( environment.PROVENANCE_ATTESTATION_PATH, ); const result = evaluatePromotionEvidence({ candidate: manifest, currentDistSha256: candidate.currentDistSha256 ?? "", localStatus: localVerification.localStatus, vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: await readTrust( environment.VULNERABILITY_PUBLIC_KEY_PATH, environment.VULNERABILITY_KEY_ID, ), provenanceTrust: await readTrust( environment.PROVENANCE_PUBLIC_KEY_PATH, environment.PROVENANCE_KEY_ID, ), }); const failures = [...candidate.failures, ...result.failures]; return Object.freeze({ schemaVersion: 1 as const, status: failures.length === 0 && result.status === "PASS" ? ("PASS" as const) : ("FAIL_UNVERIFIED" as const), vulnerabilityStatus: result.vulnerabilityStatus, provenanceAttestationStatus: result.provenanceAttestationStatus, lockfileSha256: manifest.lockfileSha256, distSha256: manifest.distSha256, failures: Object.freeze(failures), }); } async function readTrust( publicKeyPath: string | undefined, keyId: string | undefined, ): Promise { if (!publicKeyPath || !keyId?.trim()) return null; try { return Object.freeze({ keyId, publicKey: createPublicKey(await readFile(publicKeyPath, "utf8")), }); } catch { return null; } } async function optionalJson(file: string | undefined): Promise { if (!file) return null; try { return JSON.parse(await readFile(file, "utf8")) as unknown; } catch { return null; } } async function requiredJson(file: string): Promise> { const value: unknown = JSON.parse(await readFile(file, "utf8")); if (!value || typeof value !== "object" || Array.isArray(value)) { throw new TypeError(`${file} must be a JSON object`); } return value as Record; }