[READ-ONLY] a fast, modern browser for the npm registry

feat: add provenance to end of README and provenance badge (#436)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Robin <robin.kehl@singular-it.de>
Co-authored-by: Okinea Dev <hi@okinea.dev>
Co-authored-by: Daniel Roe <daniel@roe.dev>

+493 -15
+1 -1
README.md
··· 44 44 - **Fast search** &ndash; quick package search with instant results 45 45 - **Package details** &ndash; READMEs, versions, dependencies, and metadata 46 46 - **Code viewer** &ndash; browse package source code with syntax highlighting and permalink to specific lines 47 - - **Provenance indicators** &ndash; verified build badges for packages with npm provenance 47 + - **Provenance indicators** &ndash; verified build badges and provenance section below the README 48 48 - **Multi-provider repository support** &ndash; stars/forks from GitHub, GitLab, Bitbucket, Codeberg, Gitee, Sourcehut, Forgejo, Gitea, Radicle, and Tangled 49 49 - **JSR availability** &ndash; see if scoped packages are also available on JSR 50 50 - **Package badges** &ndash; module format (ESM/CJS/dual), TypeScript types (with `@types/*` links), and engine constraints
+96
app/components/PackageProvenanceSection.vue
··· 1 + <script setup lang="ts"> 2 + import type { ProvenanceDetails } from '#shared/types' 3 + 4 + defineProps<{ 5 + details: ProvenanceDetails 6 + }>() 7 + </script> 8 + 9 + <template> 10 + <section aria-labelledby="provenance-heading" class="scroll-mt-20"> 11 + <h2 id="provenance-heading" class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"> 12 + <a 13 + href="#provenance" 14 + class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline" 15 + > 16 + {{ $t('package.provenance_section.title') }} 17 + <span 18 + class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200" 19 + aria-hidden="true" 20 + /> 21 + </a> 22 + </h2> 23 + 24 + <div class="space-y-3 border border-border rounded-lg p-5"> 25 + <p class="flex items-center gap-2 text-sm text-fg m-0"> 26 + <span class="i-lucide-shield-check w-4 h-4 shrink-0 text-emerald-500" aria-hidden="true" /> 27 + <i18n-t keypath="package.provenance_section.built_and_signed_on" tag="span"> 28 + <template #provider> 29 + <strong>{{ details.providerLabel }}</strong> 30 + </template> 31 + </i18n-t> 32 + </p> 33 + <a 34 + v-if="details.buildSummaryUrl" 35 + :href="details.buildSummaryUrl" 36 + target="_blank" 37 + rel="noopener noreferrer" 38 + class="link text-sm text-fg-muted block mt-1" 39 + > 40 + {{ $t('package.provenance_section.view_build_summary') }} 41 + </a> 42 + 43 + <dl class="m-0 mt-4 flex justify-between"> 44 + <div v-if="details.sourceCommitUrl" class="flex flex-col gap-0.5"> 45 + <dt class="font-mono text-xs text-fg-muted m-0"> 46 + {{ $t('package.provenance_section.source_commit') }} 47 + </dt> 48 + <dd class="m-0"> 49 + <a 50 + :href="details.sourceCommitUrl" 51 + target="_blank" 52 + rel="noopener noreferrer" 53 + class="link font-mono text-sm break-all" 54 + > 55 + {{ 56 + details.sourceCommitSha 57 + ? `${details.sourceCommitSha.slice(0, 12)}` 58 + : details.sourceCommitUrl 59 + }} 60 + </a> 61 + </dd> 62 + </div> 63 + <div v-if="details.buildFileUrl" class="flex flex-col gap-0.5"> 64 + <dt class="font-mono text-xs text-fg-muted m-0"> 65 + {{ $t('package.provenance_section.build_file') }} 66 + </dt> 67 + <dd class="m-0"> 68 + <a 69 + :href="details.buildFileUrl" 70 + target="_blank" 71 + rel="noopener noreferrer" 72 + class="link font-mono text-sm break-all" 73 + > 74 + {{ details.buildFilePath ?? details.buildFileUrl }} 75 + </a> 76 + </dd> 77 + </div> 78 + <div v-if="details.publicLedgerUrl" class="flex flex-col gap-0.5"> 79 + <dt class="font-mono text-xs text-fg-muted m-0"> 80 + {{ $t('package.provenance_section.public_ledger') }} 81 + </dt> 82 + <dd class="m-0"> 83 + <a 84 + :href="details.publicLedgerUrl" 85 + target="_blank" 86 + rel="noopener noreferrer" 87 + class="link text-sm" 88 + > 89 + {{ $t('package.provenance_section.transparency_log_entry') }} 90 + </a> 91 + </dd> 92 + </div> 93 + </dl> 94 + </div> 95 + </section> 96 + </template>
+84 -11
app/pages/package/[...package].vue
··· 2 2 import type { 3 3 NpmVersionDist, 4 4 PackumentVersion, 5 + ProvenanceDetails, 5 6 ReadmeResponse, 6 7 SkillsListResponse, 7 8 } from '#shared/types' ··· 157 158 packageName, 158 159 () => resolvedVersion.value ?? '', 159 160 ) 161 + 162 + const { 163 + data: provenanceData, 164 + status: provenanceStatus, 165 + execute: fetchProvenance, 166 + } = useLazyFetch<ProvenanceDetails | null>( 167 + () => { 168 + const v = displayVersion.value 169 + if (!v || !hasProvenance(v)) return '' 170 + return `/api/registry/provenance/${packageName.value}/v/${v.version}` 171 + }, 172 + { 173 + default: () => null, 174 + server: false, 175 + immediate: false, 176 + }, 177 + ) 178 + if (import.meta.client) { 179 + watch( 180 + displayVersion, 181 + v => { 182 + if (v && hasProvenance(v) && provenanceStatus.value === 'idle') { 183 + fetchProvenance() 184 + } 185 + }, 186 + { immediate: true }, 187 + ) 188 + } 189 + 190 + const provenanceBadgeMounted = shallowRef(false) 191 + onMounted(() => { 192 + provenanceBadgeMounted.value = true 193 + }) 160 194 161 195 // Keep latestVersion for comparison (to show "(latest)" badge) 162 196 const latestVersion = computed(() => { ··· 523 557 > 524 558 <span v-else>v{{ resolvedVersion }}</span> 525 559 526 - <a 527 - v-if="hasProvenance(displayVersion)" 528 - :href="`https://www.npmjs.com/package/${pkg.name}/v/${resolvedVersion}#provenance`" 529 - target="_blank" 530 - rel="noopener noreferrer" 531 - class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-fg transition-colors duration-200 min-w-6 min-h-6" 532 - :title="$t('package.verified_provenance')" 533 - > 534 - <span class="i-lucide-shield-check w-3.5 h-3.5 shrink-0" aria-hidden="true" /> 535 - </a> 560 + <template v-if="hasProvenance(displayVersion) && provenanceBadgeMounted"> 561 + <TooltipApp 562 + :text=" 563 + provenanceData && provenanceStatus !== 'pending' 564 + ? $t('package.provenance_section.built_and_signed_on', { 565 + provider: provenanceData.providerLabel, 566 + }) 567 + : $t('package.verified_provenance') 568 + " 569 + position="bottom" 570 + > 571 + <a 572 + href="#provenance" 573 + :aria-label="$t('package.provenance_section.view_more_details')" 574 + class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-emerald-500 transition-colors duration-200 min-w-6 min-h-6" 575 + > 576 + <span class="i-lucide-shield-check w-3.5 h-3.5 shrink-0" aria-hidden="true" /> 577 + </a> 578 + </TooltipApp> 579 + </template> 536 580 <span 537 581 v-if="requestedVersion && latestVersion && resolvedVersion !== latestVersion.version" 538 582 class="text-fg-subtle text-sm shrink-0" ··· 1084 1128 >{{ $t('package.readme.view_on_github') }}</a 1085 1129 > 1086 1130 </p> 1131 + 1132 + <section 1133 + v-if="hasProvenance(displayVersion) && provenanceBadgeMounted" 1134 + id="provenance" 1135 + class="scroll-mt-20" 1136 + > 1137 + <div 1138 + v-if="provenanceStatus === 'pending'" 1139 + class="mt-8 flex items-center gap-2 text-fg-subtle text-sm" 1140 + > 1141 + <span 1142 + class="i-carbon-circle-dash w-4 h-4 motion-safe:animate-spin" 1143 + aria-hidden="true" 1144 + /> 1145 + <span>{{ $t('package.provenance_section.title') }}…</span> 1146 + </div> 1147 + <PackageProvenanceSection 1148 + v-else-if="provenanceData" 1149 + :details="provenanceData" 1150 + class="mt-8" 1151 + /> 1152 + <!-- Error state: provenance exists but details failed to load --> 1153 + <div 1154 + v-else-if="provenanceStatus === 'error'" 1155 + class="mt-8 flex items-center gap-2 text-fg-subtle text-sm" 1156 + > 1157 + <span class="i-carbon:warning w-4 h-4" aria-hidden="true" /> 1158 + <span>{{ $t('package.provenance_section.error_loading') }}</span> 1159 + </div> 1160 + </section> 1087 1161 </section> 1088 - 1089 1162 <div class="area-sidebar"> 1090 1163 <!-- Sidebar --> 1091 1164 <div
+13 -1
i18n/locales/en.json
··· 210 210 "view_on_github": "View on GitHub", 211 211 "toc_title": "Outline" 212 212 }, 213 + "provenance_section": { 214 + "title": "Provenance", 215 + "built_and_signed_on": "Built and signed on {provider}", 216 + "view_build_summary": "View build summary", 217 + "source_commit": "Source Commit", 218 + "build_file": "Build File", 219 + "public_ledger": "Public Ledger", 220 + "transparency_log_entry": "Transparency log entry", 221 + "view_more_details": "View more details", 222 + "error_loading": "Failed to load provenance details" 223 + }, 213 224 "keywords_title": "Keywords", 214 225 "compatibility": "Compatibility", 215 226 "card": { ··· 610 621 "provenance": { 611 622 "verified": "verified", 612 623 "verified_title": "Verified provenance", 613 - "verified_via": "Verified: published via {provider}" 624 + "verified_via": "Verified: published via {provider}", 625 + "view_more_details": "View more details" 614 626 }, 615 627 "jsr": { 616 628 "title": "also available on JSR",
+13 -1
lunaria/files/en-GB.json
··· 210 210 "view_on_github": "View on GitHub", 211 211 "toc_title": "Outline" 212 212 }, 213 + "provenance_section": { 214 + "title": "Provenance", 215 + "built_and_signed_on": "Built and signed on {provider}", 216 + "view_build_summary": "View build summary", 217 + "source_commit": "Source Commit", 218 + "build_file": "Build File", 219 + "public_ledger": "Public Ledger", 220 + "transparency_log_entry": "Transparency log entry", 221 + "view_more_details": "View more details", 222 + "error_loading": "Failed to load provenance details" 223 + }, 213 224 "keywords_title": "Keywords", 214 225 "compatibility": "Compatibility", 215 226 "card": { ··· 610 621 "provenance": { 611 622 "verified": "verified", 612 623 "verified_title": "Verified provenance", 613 - "verified_via": "Verified: published via {provider}" 624 + "verified_via": "Verified: published via {provider}", 625 + "view_more_details": "View more details" 614 626 }, 615 627 "jsr": { 616 628 "title": "also available on JSR",
+13 -1
lunaria/files/en-US.json
··· 210 210 "view_on_github": "View on GitHub", 211 211 "toc_title": "Outline" 212 212 }, 213 + "provenance_section": { 214 + "title": "Provenance", 215 + "built_and_signed_on": "Built and signed on {provider}", 216 + "view_build_summary": "View build summary", 217 + "source_commit": "Source Commit", 218 + "build_file": "Build File", 219 + "public_ledger": "Public Ledger", 220 + "transparency_log_entry": "Transparency log entry", 221 + "view_more_details": "View more details", 222 + "error_loading": "Failed to load provenance details" 223 + }, 213 224 "keywords_title": "Keywords", 214 225 "compatibility": "Compatibility", 215 226 "card": { ··· 610 621 "provenance": { 611 622 "verified": "verified", 612 623 "verified_title": "Verified provenance", 613 - "verified_via": "Verified: published via {provider}" 624 + "verified_via": "Verified: published via {provider}", 625 + "view_more_details": "View more details" 614 626 }, 615 627 "jsr": { 616 628 "title": "also available on JSR",
+1
nuxt.config.ts
··· 102 102 '/package-docs/:scope/:pkg/v/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 103 103 '/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 104 104 '/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 105 + '/api/registry/provenance/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 105 106 '/api/registry/files/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 106 107 '/_avatar/**': { 107 108 isr: 3600,
+69
server/api/registry/provenance/[...pkg].get.ts
··· 1 + import * as v from 'valibot' 2 + import { PackageRouteParamsSchema } from '#shared/schemas/package' 3 + import type { NpmVersionDist } from '#shared/types' 4 + import { CACHE_MAX_AGE_ONE_HOUR, ERROR_PROVENANCE_FETCH_FAILED } from '#shared/utils/constants' 5 + import { 6 + parseAttestationToProvenanceDetails, 7 + type NpmAttestationsResponse, 8 + } from '#server/utils/provenance' 9 + 10 + /** 11 + * GET /api/registry/provenance/:name/v/:version 12 + * 13 + * Returns parsed provenance details for a package version (build summary, source commit, build file, public ledger). 14 + * Version is required. Returns null when the version has no attestations or parsing fails. 15 + */ 16 + export default defineCachedEventHandler( 17 + async event => { 18 + const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? [] 19 + 20 + const { rawPackageName, rawVersion } = parsePackageParams(pkgParamSegments) 21 + 22 + if (!rawVersion) { 23 + throw createError({ 24 + statusCode: 400, 25 + message: 'Version is required for provenance.', 26 + }) 27 + } 28 + 29 + try { 30 + const parsed = v.parse(PackageRouteParamsSchema, { 31 + packageName: rawPackageName, 32 + version: rawVersion, 33 + }) as { packageName: string; version: string } 34 + const { packageName, version } = parsed 35 + 36 + const packument = await fetchNpmPackage(packageName) 37 + const versionData = packument.versions[version] 38 + if (!versionData) { 39 + throw createError({ 40 + statusCode: 404, 41 + message: `Version ${version} not found for package ${packageName}.`, 42 + }) 43 + } 44 + const dist = versionData.dist as NpmVersionDist | undefined 45 + const attestationsUrl = dist?.attestations?.url 46 + 47 + if (!attestationsUrl) { 48 + return null 49 + } 50 + 51 + const response = await $fetch<NpmAttestationsResponse>(attestationsUrl) 52 + const details = parseAttestationToProvenanceDetails(response) 53 + return details 54 + } catch (error: unknown) { 55 + handleApiError(error, { 56 + statusCode: 502, 57 + message: ERROR_PROVENANCE_FETCH_FAILED, 58 + }) 59 + } 60 + }, 61 + { 62 + maxAge: CACHE_MAX_AGE_ONE_HOUR, 63 + swr: true, 64 + getKey: event => { 65 + const pkg = getRouterParam(event, 'pkg') ?? '' 66 + return `provenance:v1:${pkg.replace(/\/+$/, '').trim()}` 67 + }, 68 + }, 69 + )
+143
server/utils/provenance.ts
··· 1 + import type { ProvenanceDetails } from '#shared/types' 2 + 3 + const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1' 4 + const SLSA_PROVENANCE_V0_2 = 'https://slsa.dev/provenance/v0.2' 5 + 6 + const 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> */ 15 + function 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 + 23 + const SIGSTORE_SEARCH_BASE = 'https://search.sigstore.dev' 24 + 25 + /** SLSA provenance v1 predicate; optional v0.2 fields for fallback */ 26 + interface 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 + 50 + interface AttestationItem { 51 + predicateType?: string 52 + bundle?: { 53 + dsseEnvelope?: { payload?: string } 54 + verificationMaterial?: { 55 + tlogEntries?: Array<{ logIndex?: string }> 56 + } 57 + } 58 + } 59 + 60 + export interface NpmAttestationsResponse { 61 + attestations?: AttestationItem[] 62 + } 63 + 64 + function 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 + 76 + function 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 + 83 + function 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 + */ 95 + export 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 + }
+24
shared/types/npm-registry.ts
··· 213 213 } 214 214 215 215 /** 216 + * Parsed provenance details for display (from attestation bundle SLSA predicate). 217 + * Used by the provenance API and PackageProvenanceSection. 218 + * @public 219 + */ 220 + export interface ProvenanceDetails { 221 + /** Provider ID (e.g. "github", "gitlab") */ 222 + provider: string 223 + /** Human-readable provider label (e.g. "GitHub Actions") */ 224 + providerLabel: string 225 + /** Link to build run summary (e.g. GitHub Actions run URL) */ 226 + buildSummaryUrl?: string 227 + /** Link to source commit in repository */ 228 + sourceCommitUrl?: string 229 + /** Source commit SHA (short or full) */ 230 + sourceCommitSha?: string 231 + /** Link to workflow/build config file in repo */ 232 + buildFileUrl?: string 233 + /** Workflow path (e.g. ".github/workflows/release.yml") */ 234 + buildFilePath?: string 235 + /** Link to transparency log entry (e.g. Sigstore search) */ 236 + publicLedgerUrl?: string 237 + } 238 + 239 + /** 216 240 * Download counts API response 217 241 * From https://api.npmjs.org/downloads/ 218 242 * Note: Not covered by @npm/types
+1
shared/utils/constants.ts
··· 21 21 export const NPM_MISSING_README_SENTINEL = 'ERROR: No README data found!' 22 22 export const ERROR_JSR_FETCH_FAILED = 'Failed to fetch package from JSR registry.' 23 23 export const ERROR_NPM_FETCH_FAILED = 'Failed to fetch package from npm registry.' 24 + export const ERROR_PROVENANCE_FETCH_FAILED = 'Failed to fetch provenance.' 24 25 export const UNSET_NUXT_SESSION_PASSWORD = 'NUXT_SESSION_PASSWORD not set' 25 26 export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.' 26 27 export const ERROR_SKILLS_FETCH_FAILED = 'Failed to fetch skills.'
+35
test/nuxt/a11y.spec.ts
··· 103 103 HeaderSearchBox, 104 104 LicenseDisplay, 105 105 LoadingSpinner, 106 + PackageProvenanceSection, 106 107 OrgMembersPanel, 107 108 OrgOperationsQueue, 108 109 OrgTeamsPanel, ··· 943 944 props: { 944 945 packageName: 'test-package', 945 946 open: true, 947 + }, 948 + }) 949 + const results = await runAxe(component) 950 + expect(results.violations).toEqual([]) 951 + }) 952 + }) 953 + 954 + describe('PackageProvenanceSection', () => { 955 + it('should have no accessibility violations with minimal details', async () => { 956 + const component = await mountSuspended(PackageProvenanceSection, { 957 + props: { 958 + details: { 959 + provider: 'github', 960 + providerLabel: 'GitHub Actions', 961 + }, 962 + }, 963 + }) 964 + const results = await runAxe(component) 965 + expect(results.violations).toEqual([]) 966 + }) 967 + 968 + it('should have no accessibility violations with full details', async () => { 969 + const component = await mountSuspended(PackageProvenanceSection, { 970 + props: { 971 + details: { 972 + provider: 'github', 973 + providerLabel: 'GitHub Actions', 974 + buildSummaryUrl: 'https://github.com/owner/repo/actions/runs/123', 975 + sourceCommitUrl: 'https://github.com/owner/repo/commit/abc123', 976 + sourceCommitSha: 'abc123def456', 977 + buildFileUrl: 'https://github.com/owner/repo/blob/main/.github/workflows/release.yml', 978 + buildFilePath: '.github/workflows/release.yml', 979 + publicLedgerUrl: 'https://search.sigstore.dev/example', 980 + }, 946 981 }, 947 982 }) 948 983 const results = await runAxe(component)