Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions app/components/Package/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ const { copied: copiedPkgVersion, copy: copyPkgVersion } = useClipboard({
copiedDuring: 2000,
})
function hasProvenance(version: PackumentVersion | null): boolean {
if (!version?.dist) return false
return !!(version.dist as { attestations?: unknown }).attestations
}
const publishTrustStatus = computed(
() =>
(props.resolvedVersion && props.pkg?.versions[props.resolvedVersion]?.trustStatus) || undefined,
)
const { announce } = useCommandPalette()
Expand Down Expand Up @@ -293,7 +293,23 @@ useShortcuts({
:tabindex="showScrollToTop ? 0 : -1"
/>
<div class="flex-inline items-center flex-nowrap gap-1 font-mono text-fg-muted">
<template v-if="displayVersion && hasProvenance(displayVersion)">
<template v-if="publishTrustStatus?.stagedPublish">
<TooltipApp
:text="$t('badges.staged_publish.title')"
position="bottom"
strategy="fixed"
>
<LinkBase
variant="button-secondary"
to="https://docs.npmjs.com/staged-publishing/"
:aria-label="$t('badges.staged_publish.title')"
class="py-1.25 px-2 me-2"
>
<span class="i-lucide:shield-user" aria-hidden="true" />
</LinkBase>
</TooltipApp>
</template>
<template v-if="publishTrustStatus?.provenance">
<TooltipApp
:text="
provenanceData && provenanceStatus !== 'pending'
Expand Down
29 changes: 27 additions & 2 deletions app/components/Package/Versions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ function processLoadedVersions(allVersions: PackageVersionInfo[]) {
version: v.version,
time: v.time,
tags: versionToTags.value.get(v.version),
trustStatus: v.trustStatus,
trustStatus: props.versions[v.version]?.trustStatus ?? v.trustStatus,
deprecated: v.deprecated,
}))

Expand All @@ -298,7 +298,7 @@ function processLoadedVersions(allVersions: PackageVersionInfo[]) {
version: v.version,
time: v.time,
tags: versionToTags.value.get(v.version),
trustStatus: v.trustStatus,
trustStatus: props.versions[v.version]?.trustStatus ?? v.trustStatus,
deprecated: v.deprecated,
})
}
Expand Down Expand Up @@ -641,6 +641,11 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
day="numeric"
class="text-xs text-fg-subtle"
/>
<StagedPublishBadge
v-if="row.primaryVersion.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="row.primaryVersion.trustStatus?.provenance"
:package-name="packageName"
Expand Down Expand Up @@ -695,6 +700,11 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
month="short"
day="numeric"
/>
<StagedPublishBadge
v-if="v.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="v.trustStatus?.provenance"
:package-name="packageName"
Expand Down Expand Up @@ -899,6 +909,11 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
month="short"
day="numeric"
/>
<StagedPublishBadge
v-if="group.versions[0]?.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="group.versions[0]?.trustStatus?.provenance"
:package-name="packageName"
Expand Down Expand Up @@ -966,6 +981,11 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
month="short"
day="numeric"
/>
<StagedPublishBadge
v-if="group.versions[0]?.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="group.versions[0]?.trustStatus?.provenance"
:package-name="packageName"
Expand Down Expand Up @@ -1033,6 +1053,11 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
month="short"
day="numeric"
/>
<StagedPublishBadge
v-if="v.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="v.trustStatus?.provenance"
:package-name="packageName"
Expand Down
45 changes: 45 additions & 0 deletions app/components/StagedPublishBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
compact?: boolean
linked?: boolean
}>(),
{ linked: true },
)

const stagedPublishingDocs = 'https://docs.npmjs.com/staged-publishing/'
</script>

<template>
<a
v-if="props.linked !== false"
:href="stagedPublishingDocs"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center justify-center gap-1 text-xs font-mono text-fg-muted hover:text-fg transition-colors duration-200 min-w-6 min-h-6"
:title="$t('badges.staged_publish.title')"
>
<span
class="i-lucide:shield-user shrink-0"
:class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'"
aria-hidden="true"
/>
<span v-if="!compact" class="sr-only sm:not-sr-only">
{{ $t('badges.staged_publish.label') }}
</span>
</a>
<span
v-else
class="inline-flex items-center gap-1 text-xs font-mono text-fg-muted"
:title="$t('badges.staged_publish.title')"
>
<span
class="i-lucide:shield-user shrink-0"
:class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'"
aria-hidden="true"
/>
<span v-if="!compact" class="sr-only sm:not-sr-only">
{{ $t('badges.staged_publish.label') }}
</span>
</span>
</template>
15 changes: 15 additions & 0 deletions app/pages/package/[[org]]/[name]/versions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,11 @@ const flatItems = computed<FlatItem[]>(() => {
dir="ltr"
>v{{ latestTagRow!.version }}</LinkBase
>
<StagedPublishBadge
v-if="fullVersionMap?.get(latestTagRow!.version)?.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="fullVersionMap?.get(latestTagRow!.version)?.trustStatus?.provenance"
:package-name="packageName"
Expand Down Expand Up @@ -403,6 +408,11 @@ const flatItems = computed<FlatItem[]>(() => {
>
v{{ row.version }}
</LinkBase>
<StagedPublishBadge
v-if="fullVersionMap?.get(row.version)?.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="fullVersionMap?.get(row.version)?.trustStatus?.provenance"
:package-name="packageName"
Expand Down Expand Up @@ -617,6 +627,11 @@ const flatItems = computed<FlatItem[]>(() => {
>
v{{ item.version }}
</LinkBase>
<StagedPublishBadge
v-if="fullVersionMap?.get(item.version)?.trustStatus?.stagedPublish"
compact
class="relative z-10"
/>
<ProvenanceBadge
v-if="fullVersionMap?.get(item.version)?.trustStatus?.provenance"
:package-name="packageName"
Expand Down
7 changes: 5 additions & 2 deletions app/utils/npm/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ export async function fetchAllPackageVersions(packageName: string): Promise<Pack
.map(([version, meta]) => ({
version,
time: meta.time,
hasProvenance: meta.provenance,
hasTrustedPublisher: meta.trustedPublisher,
trustStatus: {
provenance: !!meta.provenance,
trustedPublisher: !!meta.trustedPublisher,
stagedPublish: !!meta.staged,
},
deprecated: meta.deprecated,
}))
.sort((a, b) => compare(b.version, a.version))
Expand Down
4 changes: 4 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,10 @@
"verified_title": "Verified provenance",
"verified_via": "Verified: published via {provider}"
},
"staged_publish": {
"label": "staged publish",
"title": "Published through staged publishing with 2FA approval"
},
"jsr": {
"title": "also available on JSR"
}
Expand Down
12 changes: 12 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3304,6 +3304,18 @@
},
"additionalProperties": false
},
"staged_publish": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"title": {
"type": "string"
}
},
"additionalProperties": false
},
"jsr": {
"type": "object",
"properties": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"algoliasearch": "5.55.1",
"defu": "6.1.7",
"diff": "^9.0.0",
"fast-npm-meta": "2.1.0",
"fast-npm-meta": "2.2.0",
"focus-trap": "^8.0.0",
"gray-matter": "4.0.3",
"hls.js": "1.6.16",
Expand Down
12 changes: 7 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion shared/types/npm-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export type { Manifest, ManifestVersion, PackageJSON } from '@npm/types'
type NpmTrustedPublisherEvidence = NpmSearchTrustedPublisher | NpmTrustedPublisher | true

export interface PackumentVersion extends PackumentVersionWithoutAttestations {
_npmUser?: Contact & { trustedPublisher?: NpmTrustedPublisherEvidence }
_npmUser?: Contact & {
trustedPublisher?: NpmTrustedPublisherEvidence
/** Present when the version was released through staged publishing. */
approver?: Contact
}
dist: PackumentVersionWithoutAttestations['dist'] & { attestations?: NpmVersionAttestations }
}

Expand Down
17 changes: 17 additions & 0 deletions test/nuxt/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ import {
PaginationControls,
ProgressBar,
ProvenanceBadge,
StagedPublishBadge,
Readme,
ReadmeTocDropdown,
SearchProviderToggle,
Expand Down Expand Up @@ -1006,6 +1007,22 @@ describe('component accessibility audits', () => {
})
})

describe('StagedPublishBadge', () => {
it('should have no accessibility violations with link', async () => {
const component = await mountSuspended(StagedPublishBadge)
const results = await runAxe(component)
expect(results.violations).toEqual([])
})

it('should have no accessibility violations without link', async () => {
const component = await mountSuspended(StagedPublishBadge, {
props: { linked: false },
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('PackageSkeleton', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(PackageSkeleton)
Expand Down
37 changes: 35 additions & 2 deletions test/nuxt/components/Package/Versions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ function createVersion(
options: {
deprecated?: string
hasProvenance?: boolean
hasStagedPublish?: boolean
} = {},
): SlimVersion {
return {
version,
deprecated: options.deprecated,
tags: undefined,
...(options.hasProvenance
? { trustStatus: { provenance: true, trustedPublisher: false, stagedPublish: false } }
...(options.hasProvenance || options.hasStagedPublish
? {
trustStatus: {
provenance: !!options.hasProvenance,
trustedPublisher: false,
stagedPublish: !!options.hasStagedPublish,
},
}
: {}),
} as SlimVersion
}
Expand Down Expand Up @@ -389,6 +396,32 @@ describe('PackageVersions', () => {
const provenanceBadge = component.findComponent({ name: 'ProvenanceBadge' })
expect(provenanceBadge.exists()).toBe(false)
})

it('shows staged publishing when a version was approved from staging', async () => {
const component = await mountSuspended(PackageVersions, {
props: {
packageName: 'test-package',
versions: {
'1.0.0': createVersion('1.0.0', {
hasProvenance: true,
hasStagedPublish: true,
}),
},
distTags: { latest: '1.0.0' },
time: { '1.0.0': '2026-08-03T12:00:00.000Z' },
},
})

const badge = component.findComponent({ name: 'StagedPublishBadge' })
expect(badge.exists()).toBe(true)
expect(badge.get('a').attributes()).toMatchObject({
href: 'https://docs.npmjs.com/staged-publishing/',
target: '_blank',
rel: 'noopener noreferrer',
title: 'Published through staged publishing with 2FA approval',
})
expect(component.findComponent({ name: 'ProvenanceBadge' }).exists()).toBe(true)
})
})

describe('datetime display', () => {
Expand Down
Loading
Loading