[READ-ONLY] a fast, modern browser for the npm registry
at main 143 lines 5.3 kB view raw
1import type { ProvenanceDetails } from '#shared/types' 2 3const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1' 4const SLSA_PROVENANCE_V0_2 = 'https://slsa.dev/provenance/v0.2' 5 6const PROVIDER_IDS: Record<string, { provider: string; providerLabel: string }> = { 7 'https://github.com/actions/runner/github-hosted': { 8 provider: 'github', 9 providerLabel: 'GitHub Actions', 10 }, 11 'https://github.com/actions/runner': { provider: 'github', providerLabel: 'GitHub Actions' }, 12} 13 14/** GitLab uses project-specific builder IDs: https://gitlab.com/<path>/-/runners/<id> */ 15function getProviderInfo(builderId: string): { provider: string; providerLabel: string } { 16 const exact = PROVIDER_IDS[builderId] 17 if (exact) return exact 18 if (builderId.includes('gitlab.com') && builderId.includes('/runners/')) 19 return { provider: 'gitlab', providerLabel: 'GitLab CI' } 20 return { provider: 'unknown', providerLabel: builderId ? 'CI' : 'Unknown' } 21} 22 23const SIGSTORE_SEARCH_BASE = 'https://search.sigstore.dev' 24 25/** SLSA provenance v1 predicate; optional v0.2 fields for fallback */ 26interface SlsaPredicate { 27 buildDefinition?: { 28 externalParameters?: { 29 workflow?: { 30 repository?: string 31 path?: string 32 ref?: string 33 } 34 } 35 resolvedDependencies?: Array<{ 36 uri?: string 37 digest?: { gitCommit?: string } 38 }> 39 } 40 runDetails?: { 41 builder?: { id?: string } 42 metadata?: { invocationId?: string } 43 } 44 /** v0.2 */ 45 builder?: { id?: string } 46 /** v0.2 */ 47 metadata?: { buildInvocationId?: string } 48} 49 50interface AttestationItem { 51 predicateType?: string 52 bundle?: { 53 dsseEnvelope?: { payload?: string } 54 verificationMaterial?: { 55 tlogEntries?: Array<{ logIndex?: string }> 56 } 57 } 58} 59 60export interface NpmAttestationsResponse { 61 attestations?: AttestationItem[] 62} 63 64function decodePayload( 65 payloadBase64: string | undefined, 66): { predicateType?: string; predicate?: SlsaPredicate } | null { 67 if (!payloadBase64 || typeof payloadBase64 !== 'string') return null 68 try { 69 const decoded = Buffer.from(payloadBase64, 'base64').toString('utf-8') 70 return JSON.parse(decoded) as { predicateType?: string; predicate?: SlsaPredicate } 71 } catch { 72 return null 73 } 74} 75 76function repoUrlToCommitUrl(repository: string, sha: string): string { 77 const normalized = repository.replace(/\/$/, '').replace(/\.git$/, '') 78 if (normalized.includes('github.com')) return `${normalized}/commit/${sha}` 79 if (normalized.includes('gitlab.com')) return `${normalized}/-/commit/${sha}` 80 return `${normalized}/commit/${sha}` 81} 82 83function repoUrlToBlobUrl(repository: string, path: string, ref = 'main'): string { 84 const normalized = repository.replace(/\/$/, '').replace(/\.git$/, '') 85 if (normalized.includes('github.com')) return `${normalized}/blob/${ref}/${path}` 86 if (normalized.includes('gitlab.com')) return `${normalized}/-/blob/${ref}/${path}` 87 return `${normalized}/blob/${ref}/${path}` 88} 89 90/** 91 * Parse npm attestations API response into ProvenanceDetails. 92 * Prefers SLSA provenance v1; falls back to v0.2 for provider label and ledger only (no source commit/build file from v0.2). 93 * @public 94 */ 95export function parseAttestationToProvenanceDetails(response: unknown): ProvenanceDetails | null { 96 const body = response as NpmAttestationsResponse 97 const list = body?.attestations 98 if (!Array.isArray(list)) return null 99 100 const slsaAttestation = 101 list.find(a => a.predicateType === SLSA_PROVENANCE_V1) ?? 102 list.find(a => a.predicateType === SLSA_PROVENANCE_V0_2) 103 if (!slsaAttestation?.bundle?.dsseEnvelope) return null 104 105 const payload = decodePayload(slsaAttestation.bundle.dsseEnvelope.payload) 106 if (!payload?.predicate) return null 107 108 const pred = payload.predicate as SlsaPredicate 109 const builderId = pred.runDetails?.builder?.id ?? pred.builder?.id ?? '' 110 const providerInfo = getProviderInfo(builderId) 111 112 const workflow = pred.buildDefinition?.externalParameters?.workflow 113 const repo = workflow?.repository?.replace(/\/$/, '').replace(/\.git$/, '') ?? '' 114 const workflowPath = workflow?.path ?? '' 115 const ref = workflow?.ref?.replace(/^refs\/heads\//, '').replace(/^refs\/tags\//, '') ?? 'main' 116 117 const resolved = pred.buildDefinition?.resolvedDependencies?.[0] 118 const commitSha = resolved?.digest?.gitCommit ?? '' 119 120 const rawInvocationId = 121 pred.runDetails?.metadata?.invocationId ?? pred.metadata?.buildInvocationId 122 const buildSummaryUrl = 123 rawInvocationId?.startsWith('http://') || rawInvocationId?.startsWith('https://') 124 ? rawInvocationId 125 : undefined 126 const sourceCommitUrl = repo && commitSha ? repoUrlToCommitUrl(repo, commitSha) : undefined 127 const buildFileUrl = repo && workflowPath ? repoUrlToBlobUrl(repo, workflowPath, ref) : undefined 128 129 const tlogEntries = slsaAttestation.bundle.verificationMaterial?.tlogEntries 130 const logIndex = tlogEntries?.[0]?.logIndex 131 const publicLedgerUrl = logIndex ? `${SIGSTORE_SEARCH_BASE}/?logIndex=${logIndex}` : undefined 132 133 return { 134 provider: providerInfo.provider, 135 providerLabel: providerInfo.providerLabel, 136 buildSummaryUrl, 137 sourceCommitUrl, 138 sourceCommitSha: commitSha || undefined, 139 buildFileUrl, 140 buildFilePath: workflowPath || undefined, 141 publicLedgerUrl, 142 } 143}