#!/usr/bin/env bun /** * SLSA Provenance generation script for CI/CD * * Generates an in-toto attestation with SLSA v1.0 provenance predicate * for a published package. Intended to be called from the publish pipeline * after a successful npm publish. * * Reads build metadata from GitLab CI environment variables and writes * the provenance statement to a `.provenance.json` file alongside the artifact. * * Usage: * bun scripts/generate-provenance.ts --package @suaveplan/utils --version 1.2.3 --digest abc123 * bun scripts/generate-provenance.ts --package @suaveplan/utils --version 1.2.3 --digest abc123 --output ./provenance/ * * Environment variables (auto-detected in GitLab CI): * CI_COMMIT_SHA - Git commit SHA * CI_PROJECT_URL - Repository URL * CI_COMMIT_REF_NAME - Branch or tag name * CI_RUNNER_ID - Runner identifier * CI_JOB_ID - Job identifier * CI_JOB_STARTED_AT - Job start timestamp * CI_PIPELINE_ID - Pipeline identifier */ import { createHash } from "node:crypto"; import { mkdir, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { CONFIG } from "./lib/config.js"; const ROOT_DIR = CONFIG.paths.root; interface CliOptions { packageName: string; version: string; digest: string; outputDir?: string; } function parseArgs(): CliOptions { const args = process.argv.slice(2); let packageName = ""; let version = ""; let digest = ""; let outputDir: string | undefined; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--package" && i + 1 < args.length) { packageName = args[++i]; } else if (arg.startsWith("--package=")) { packageName = arg.slice(10); } else if (arg === "--version" && i + 1 < args.length) { version = args[++i]; } else if (arg.startsWith("--version=")) { version = arg.slice(10); } else if (arg === "--digest" && i + 1 < args.length) { digest = args[++i]; } else if (arg.startsWith("--digest=")) { digest = arg.slice(9); } else if (arg === "--output" && i + 1 < args.length) { outputDir = args[++i]; } else if (arg.startsWith("--output=")) { outputDir = arg.slice(9); } } if (!packageName || !version || !digest) { console.error( "Usage: bun scripts/generate-provenance.ts --package --version --digest " ); process.exit(1); } return { packageName, version, digest, outputDir }; } function getGitLabEnv() { return { commitSha: process.env.CI_COMMIT_SHA ?? "unknown", projectUrl: process.env.CI_PROJECT_URL ?? "https://gitlab.fq.io/suaveplan/monorepo", refName: process.env.CI_COMMIT_REF_NAME ?? "unknown", runnerId: process.env.CI_RUNNER_ID ?? "local", jobId: process.env.CI_JOB_ID ?? `local-${Date.now()}`, jobStartedAt: process.env.CI_JOB_STARTED_AT ?? new Date().toISOString(), pipelineId: process.env.CI_PIPELINE_ID ?? "local", isCI: !!(process.env.CI || process.env.GITLAB_CI), }; } function generateProvenanceStatement( options: CliOptions, env: ReturnType ) { const buildType = "https://suaveplan.app/slsa/build-type/bun-monorepo/v1"; const encodedName = options.packageName.replace("@", "%40"); const subjectUri = `pkg:npm/${encodedName}@${options.version}`; const builderId = `${env.projectUrl}/-/runners/${env.runnerId}`; const invocationId = `${env.projectUrl}/-/pipelines/${env.pipelineId}/jobs/${env.jobId}`; return { _type: "https://in-toto.io/Statement/v1", subject: [ { uri: subjectUri, digest: { sha256: options.digest }, name: options.packageName, }, ], predicateType: "https://slsa.dev/provenance/v1", predicate: { buildDefinition: { buildType, externalParameters: { repository: env.projectUrl, ref: `refs/heads/${env.refName}`, }, internalParameters: { environment: env.isCI ? "gitlab-ci" : "local", resolvedDependencies: [ { uri: `git+${env.projectUrl}@refs/heads/${env.refName}`, digest: { sha256: env.commitSha }, name: "source", }, ], }, resolvedDependencies: [ { uri: `git+${env.projectUrl}@refs/heads/${env.refName}`, digest: { sha256: env.commitSha }, name: "source", }, ], }, runDetails: { builder: { id: builderId, version: { bun: typeof Bun !== "undefined" ? Bun.version : "unknown", }, }, metadata: { invocationId, startedOn: env.jobStartedAt, finishedOn: new Date().toISOString(), }, }, }, }; } async function main() { const options = parseArgs(); const env = getGitLabEnv(); console.log( `Generating SLSA provenance for ${options.packageName}@${options.version}` ); console.log(` Commit: ${env.commitSha}`); console.log(` Builder: ${env.projectUrl}/-/runners/${env.runnerId}`); console.log(` CI: ${env.isCI ? "yes" : "no (local)"}`); const statement = generateProvenanceStatement(options, env); const json = JSON.stringify(statement, null, 2); // Compute digest of the provenance itself const provenanceDigest = createHash("sha256").update(json).digest("hex"); const outputDir = options.outputDir ? join(ROOT_DIR, options.outputDir) : join(ROOT_DIR, "provenance"); try { await stat(outputDir); } catch { await mkdir(outputDir, { recursive: true }); } const safeName = options.packageName.replace(/[/@]/g, "_"); const filename = `${safeName}-${options.version}.provenance.json`; const outputPath = join(outputDir, filename); await writeFile(outputPath, json); console.log(` Output: ${outputPath}`); console.log(` Provenance digest: ${provenanceDigest}`); } main().catch(console.error);