diff --git a/app/components/Terminal/Install.vue b/app/components/Terminal/Install.vue index 5fe249ea0..273960c0f 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 ae99b0afc..dd89412aa 100644 --- a/app/composables/npm/usePackage.ts +++ b/app/composables/npm/usePackage.ts @@ -1,9 +1,30 @@ -import type { Packument, SlimPackument, SlimVersion, SlimPackumentVersion } from '#shared/types' +import type { + Packument, + SlimPackument, + SlimVersion, + SlimPackumentVersion, + PackumentVersion, + PublishTrustLevel, +} from '#shared/types' 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: @@ -11,7 +32,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'] ?? {})) @@ -34,6 +58,23 @@ function transformPackument(pkg: Packument, requestedVersion?: string | null): S includedVersions.add(requestedVersion) } + // Build security metadata for all versions, but only include in payload + // when the package has mixed trust levels (i.e. a downgrade could exist) + const securityVersionEntries = Object.entries(pkg.versions).map(([version, metadata]) => { + const trustLevel = getTrustLevel(metadata) + return { + version, + time: pkg.time[version], + hasProvenance: trustLevel !== 'none', + trustLevel, + deprecated: metadata.deprecated, + } + }) + + const trustLevels = new Set(securityVersionEntries.map(v => v.trustLevel)) + const hasMixedTrust = trustLevels.size > 1 + const securityVersions = hasMixedTrust ? securityVersionEntries : undefined + // Build filtered versions object with install scripts info per version const filteredVersions: Record = {} let versionData: SlimPackumentVersion | null = null @@ -51,8 +92,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[], @@ -90,6 +135,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/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 913c54af3..1722dc0b9 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -1,6 +1,7 @@