diff --git a/app/components/Terminal/Install.vue b/app/components/Terminal/Install.vue index b03a5d6fa..6a8f2fd62 100644 --- a/app/components/Terminal/Install.vue +++ b/app/components/Terminal/Install.vue @@ -5,6 +5,7 @@ import type { PackageManagerId } from '~/utils/install-command' const props = defineProps<{ packageName: string requestedVersion?: string | null + installVersionOverride?: string | null jsrInfo?: JsrPackageInfo | null typesPackageName?: string | null executableInfo?: { hasExecutable: boolean; primaryCommand?: string } | null @@ -16,6 +17,7 @@ const { selectedPM, showTypesInInstall, copied, copyInstallCommand } = useInstal () => props.requestedVersion ?? null, () => props.jsrInfo ?? null, () => props.typesPackageName ?? null, + () => props.installVersionOverride ?? null, ) // Generate install command parts for a specific package manager @@ -23,7 +25,7 @@ function getInstallPartsForPM(pmId: PackageManagerId) { return getInstallCommandParts({ packageName: props.packageName, packageManager: pmId, - version: props.requestedVersion, + version: props.installVersionOverride ?? props.requestedVersion, jsrInfo: props.jsrInfo, }) } diff --git a/app/composables/npm/usePackage.ts b/app/composables/npm/usePackage.ts index e0d900f50..2dfbb9c48 100644 --- a/app/composables/npm/usePackage.ts +++ b/app/composables/npm/usePackage.ts @@ -1,10 +1,31 @@ -import type { Packument, SlimPackument, SlimVersion, SlimPackumentVersion } from '#shared/types' +import type { + Packument, + SlimPackument, + SlimVersion, + SlimPackumentVersion, + PackumentVersion, + PublishTrustLevel, +} from '#shared/types' import { NPM_REGISTRY } from '~/utils/npm/common' import { extractInstallScriptsInfo } from '~/utils/install-scripts' /** Number of recent versions to include in initial payload */ const RECENT_VERSIONS_COUNT = 5 +function hasAttestations(version: PackumentVersion): boolean { + return Boolean(version.dist.attestations) +} + +function hasTrustedPublisher(version: PackumentVersion): boolean { + return Boolean(version._npmUser?.trustedPublisher) +} + +function getTrustLevel(version: PackumentVersion): PublishTrustLevel { + if (hasAttestations(version)) return 'provenance' + if (hasTrustedPublisher(version)) return 'trustedPublisher' + return 'none' +} + /** * Transform a full Packument into a slimmed version for client-side use. * Reduces payload size by: @@ -12,7 +33,10 @@ const RECENT_VERSIONS_COUNT = 5 * - Including only: 5 most recent versions + one version per dist-tag + requested version * - Stripping unnecessary fields from version objects */ -function transformPackument(pkg: Packument, requestedVersion?: string | null): SlimPackument { +export function transformPackument( + pkg: Packument, + requestedVersion?: string | null, +): SlimPackument { // Get versions pointed to by dist-tags const distTagVersions = new Set(Object.values(pkg['dist-tags'] ?? {})) @@ -35,6 +59,17 @@ function transformPackument(pkg: Packument, requestedVersion?: string | null): S includedVersions.add(requestedVersion) } + const securityVersions = Object.entries(pkg.versions).map(([version, metadata]) => { + const trustLevel = getTrustLevel(metadata) + return { + version, + time: pkg.time[version], + hasProvenance: trustLevel !== 'none', + trustLevel, + deprecated: metadata.deprecated, + } + }) + // Build filtered versions object with install scripts info per version const filteredVersions: Record = {} let versionData: SlimPackumentVersion | null = null @@ -52,8 +87,12 @@ function transformPackument(pkg: Packument, requestedVersion?: string | null): S installScripts: installScripts ?? undefined, } } + const trustLevel = getTrustLevel(version) + const hasProvenance = trustLevel !== 'none' + filteredVersions[v] = { - ...((version?.dist as { attestations?: unknown }) ? { hasProvenance: true } : {}), + hasProvenance, + trustLevel, version: version.version, deprecated: version.deprecated, tags: version.tags as string[], @@ -91,6 +130,7 @@ function transformPackument(pkg: Packument, requestedVersion?: string | null): S 'bugs': pkg.bugs, 'requestedVersion': versionData, 'versions': filteredVersions, + 'securityVersions': securityVersions, } } diff --git a/app/composables/useInstallCommand.ts b/app/composables/useInstallCommand.ts index 23094d457..827ac89f4 100644 --- a/app/composables/useInstallCommand.ts +++ b/app/composables/useInstallCommand.ts @@ -9,6 +9,7 @@ export function useInstallCommand( requestedVersion: MaybeRefOrGetter, jsrInfo: MaybeRefOrGetter, typesPackageName: MaybeRefOrGetter, + installVersionOverride?: MaybeRefOrGetter, ) { const selectedPM = useSelectedPackageManager() const { settings } = useSettings() @@ -21,10 +22,11 @@ export function useInstallCommand( const installCommandParts = computed(() => { const name = toValue(packageName) if (!name) return [] + const version = toValue(installVersionOverride) ?? toValue(requestedVersion) return getInstallCommandParts({ packageName: name, packageManager: selectedPM.value, - version: toValue(requestedVersion), + version, jsrInfo: toValue(jsrInfo), }) }) @@ -32,10 +34,11 @@ export function useInstallCommand( const installCommand = computed(() => { const name = toValue(packageName) if (!name) return '' + const version = toValue(installVersionOverride) ?? toValue(requestedVersion) return getInstallCommand({ packageName: name, packageManager: selectedPM.value, - version: toValue(requestedVersion), + version, jsrInfo: toValue(jsrInfo), }) }) diff --git a/app/pages/package/[...package].vue b/app/pages/package/[...package].vue index ac08f83a1..a85fd61c9 100644 --- a/app/pages/package/[...package].vue +++ b/app/pages/package/[...package].vue @@ -1,6 +1,7 @@