Skip to content
Merged
4 changes: 3 additions & 1 deletion app/components/Terminal/Install.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,14 +17,15 @@ 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
function getInstallPartsForPM(pmId: PackageManagerId) {
return getInstallCommandParts({
packageName: props.packageName,
packageManager: pmId,
version: props.requestedVersion,
version: props.installVersionOverride ?? props.requestedVersion,
jsrInfo: props.jsrInfo,
})
}
Expand Down
52 changes: 49 additions & 3 deletions app/composables/npm/usePackage.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
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:
* - Removing readme (fetched separately)
* - 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'] ?? {}))

Expand All @@ -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<string, SlimVersion> = {}
let versionData: SlimPackumentVersion | null = null
Expand All @@ -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,
Comment on lines +95 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same hasProvenance conflation applies here in the filtered versions.

Lines 90–91 replicate the same trustLevel !== 'none'hasProvenance pattern flagged above. This should use hasAttestations(version) for consistency and correctness.

Proposed fix
-      const trustLevel = getTrustLevel(version)
-      const hasProvenance = trustLevel !== 'none'
+      const trustLevel = getTrustLevel(version)
+      const hasProvenance = hasAttestations(version)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const trustLevel = getTrustLevel(version)
const hasProvenance = trustLevel !== 'none'
filteredVersions[v] = {
...((version?.dist as { attestations?: unknown }) ? { hasProvenance: true } : {}),
hasProvenance,
trustLevel,
const trustLevel = getTrustLevel(version)
const hasProvenance = hasAttestations(version)
filteredVersions[v] = {
hasProvenance,
trustLevel,

version: version.version,
deprecated: version.deprecated,
tags: version.tags as string[],
Expand Down Expand Up @@ -90,6 +135,7 @@ function transformPackument(pkg: Packument, requestedVersion?: string | null): S
'bugs': pkg.bugs,
'requestedVersion': versionData,
'versions': filteredVersions,
'securityVersions': securityVersions,
}
}

Expand Down
7 changes: 5 additions & 2 deletions app/composables/useInstallCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function useInstallCommand(
requestedVersion: MaybeRefOrGetter<string | null>,
jsrInfo: MaybeRefOrGetter<JsrPackageInfo | null>,
typesPackageName: MaybeRefOrGetter<string | null>,
installVersionOverride?: MaybeRefOrGetter<string | null>,
) {
const selectedPM = useSelectedPackageManager()
const { settings } = useSettings()
Expand All @@ -21,21 +22,23 @@ 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),
})
})

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),
})
})
Expand Down
125 changes: 125 additions & 0 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type {
NpmVersionDist,
PackageVersionInfo,
PackumentVersion,
ProvenanceDetails,
ReadmeResponse,
Expand All @@ -12,6 +13,7 @@ import { joinURL } from 'ufo'
import { areUrlsEquivalent } from '#shared/utils/url'
import { isEditableElement } from '~/utils/input'
import { getDependencyCount } from '~/utils/npm/dependency-count'
import { detectPublishSecurityDowngradeForVersion } from '~/utils/publish-security'
import { useModal } from '~/composables/useModal'
import { useAtproto } from '~/composables/atproto/useAtproto'
import { togglePackageLike } from '~/utils/atproto/likes'
Expand Down Expand Up @@ -143,6 +145,18 @@ const {
error,
} = usePackage(packageName, () => resolvedVersion.value ?? requestedVersion.value)
const displayVersion = computed(() => pkg.value?.requestedVersion ?? null)
const versionSecurityMetadata = computed<PackageVersionInfo[]>(() => {
if (!pkg.value) return []
if (pkg.value.securityVersions?.length) return pkg.value.securityVersions

return Object.entries(pkg.value.versions).map(([version, metadata]) => ({
version,
time: pkg.value?.time?.[version],
hasProvenance: !!metadata.hasProvenance,
trustLevel: metadata.trustLevel,
deprecated: metadata.deprecated,
}))
})

// Process package description
const pkgDescription = useMarkdown(() => ({
Expand Down Expand Up @@ -225,6 +239,30 @@ const deprecationNoticeMessage = useMarkdown(() => ({
text: deprecationNotice.value?.message ?? '',
}))

const publishSecurityDowngrade = computed(() => {
const currentVersion = displayVersion.value?.version
if (!currentVersion) return null
return detectPublishSecurityDowngradeForVersion(versionSecurityMetadata.value, currentVersion)
})

const installVersionOverride = computed(
() => publishSecurityDowngrade.value?.trustedVersion ?? null,
)

const downgradeFallbackInstallText = computed(() => {
const d = publishSecurityDowngrade.value
if (!d?.trustedVersion) return null
if (d.trustedTrustLevel === 'provenance')
return $t('package.security_downgrade.fallback_install_provenance', {
version: d.trustedVersion,
})
if (d.trustedTrustLevel === 'trustedPublisher')
return $t('package.security_downgrade.fallback_install_trustedPublisher', {
version: d.trustedVersion,
})
return null
})

const sizeTooltip = computed(() => {
const chunks = [
displayVersion.value &&
Expand Down Expand Up @@ -1020,9 +1058,96 @@ onKeyStroke(
:id="`pm-panel-${activePmId}`"
:aria-labelledby="`pm-tab-${activePmId}`"
>
<div
v-if="publishSecurityDowngrade"
role="alert"
class="mb-4 rounded-lg border border-amber-600/40 bg-amber-500/10 px-4 py-3 text-amber-700 dark:text-amber-400"
>
<h3 class="m-0 flex items-center gap-2 font-mono text-sm font-medium">
<span class="i-carbon:warning-alt w-4 h-4 shrink-0" aria-hidden="true" />
{{ $t('package.security_downgrade.title') }}
</h3>
<p class="mt-2 mb-0 text-sm">
<i18n-t
v-if="
publishSecurityDowngrade.downgradedTrustLevel === 'none' &&
publishSecurityDowngrade.trustedTrustLevel === 'provenance'
"
keypath="package.security_downgrade.description_to_none_provenance"
tag="span"
scope="global"
>
<template #provenance>
<a
href="https://docs.npmjs.com/generating-provenance-statements"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-sm underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg focus-visible:decoration-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 transition-colors"
>{{ $t('package.security_downgrade.provenance_link_text')
}}<span class="i-carbon-launch w-3 h-3" aria-hidden="true"
/></a>
</template>
</i18n-t>
<i18n-t
v-else-if="
publishSecurityDowngrade.downgradedTrustLevel === 'none' &&
publishSecurityDowngrade.trustedTrustLevel === 'trustedPublisher'
"
keypath="package.security_downgrade.description_to_none_trustedPublisher"
tag="span"
scope="global"
>
<template #trustedPublishing>
<a
href="https://docs.npmjs.com/adding-a-trusted-publisher-to-a-package"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-sm underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg focus-visible:decoration-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 transition-colors"
>{{ $t('package.security_downgrade.trusted_publishing_link_text')
}}<span class="i-carbon-launch w-3 h-3" aria-hidden="true"
/></a>
</template>
</i18n-t>
<i18n-t
v-else-if="
publishSecurityDowngrade.downgradedTrustLevel === 'provenance' &&
publishSecurityDowngrade.trustedTrustLevel === 'trustedPublisher'
"
keypath="package.security_downgrade.description_to_provenance_trustedPublisher"
tag="span"
scope="global"
>
<template #provenance>
<a
href="https://docs.npmjs.com/generating-provenance-statements"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-sm underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg focus-visible:decoration-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 transition-colors"
>{{ $t('package.security_downgrade.provenance_link_text')
}}<span class="i-carbon-launch w-3 h-3" aria-hidden="true"
/></a>
</template>
<template #trustedPublishing>
<a
href="https://docs.npmjs.com/adding-a-trusted-publisher-to-a-package"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-sm underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg focus-visible:decoration-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 transition-colors"
>{{ $t('package.security_downgrade.trusted_publishing_link_text')
}}<span class="i-carbon-launch w-3 h-3" aria-hidden="true"
/></a>
</template>
</i18n-t>
{{ ' ' }}
<template v-if="downgradeFallbackInstallText">
{{ downgradeFallbackInstallText }}
</template>
</p>
</div>
<TerminalInstall
:package-name="pkg.name"
:requested-version="requestedVersion"
:install-version-override="installVersionOverride"
:jsr-info="jsrInfo"
:types-package-name="typesPackageName"
:executable-info="executableInfo"
Expand Down
Loading
Loading