forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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}