|
2 | 2 | import type { |
3 | 3 | NpmVersionDist, |
4 | 4 | PackumentVersion, |
| 5 | + ProvenanceDetails, |
5 | 6 | ReadmeResponse, |
6 | 7 | SkillsListResponse, |
7 | 8 | } from '#shared/types' |
@@ -158,6 +159,39 @@ const { data: vulnTree, status: vulnTreeStatus } = useDependencyAnalysis( |
158 | 159 | () => resolvedVersion.value ?? '', |
159 | 160 | ) |
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 | +}) |
| 194 | +
|
161 | 195 | // Keep latestVersion for comparison (to show "(latest)" badge) |
162 | 196 | const latestVersion = computed(() => { |
163 | 197 | if (!pkg.value) return null |
@@ -523,16 +557,26 @@ defineOgImageComponent('Package', { |
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,8 +1128,37 @@ defineOgImageComponent('Package', { |
1084 | 1128 | >{{ $t('package.readme.view_on_github') }}</a |
1085 | 1129 | > |
1086 | 1130 | </p> |
1087 | | - </section> |
1088 | 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> |
| 1161 | + </section> |
1089 | 1162 | <div class="area-sidebar"> |
1090 | 1163 | <!-- Sidebar --> |
1091 | 1164 | <div |
|
0 commit comments