diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue
index 1e0a0e813f..7fae62c643 100644
--- a/app/components/Package/Header.vue
+++ b/app/components/Package/Header.vue
@@ -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()
@@ -284,7 +284,23 @@ useShortcuts({
:tabindex="showScrollToTop ? 0 : -1"
/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+const props = withDefaults(
+ defineProps<{
+ compact?: boolean
+ linked?: boolean
+ }>(),
+ { linked: true },
+)
+
+const stagedPublishingDocs = 'https://docs.npmjs.com/staged-publishing/'
+
+
+
+
+
+
+ {{ $t('badges.staged_publish.label') }}
+
+
+
+
+
+ {{ $t('badges.staged_publish.label') }}
+
+
+
diff --git a/app/pages/package/[[org]]/[name]/versions.vue b/app/pages/package/[[org]]/[name]/versions.vue
index fdbe0a0172..658e25a48f 100644
--- a/app/pages/package/[[org]]/[name]/versions.vue
+++ b/app/pages/package/[[org]]/[name]/versions.vue
@@ -329,6 +329,11 @@ const flatItems = computed(() => {
dir="ltr"
>v{{ latestTagRow!.version }}
+
(() => {
>
v{{ row.version }}
+
(() => {
>
v{{ item.version }}
+
{
})
})
+ 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)
diff --git a/test/nuxt/components/Package/Versions.spec.ts b/test/nuxt/components/Package/Versions.spec.ts
index ff8220391b..04ecc5026e 100644
--- a/test/nuxt/components/Package/Versions.spec.ts
+++ b/test/nuxt/components/Package/Versions.spec.ts
@@ -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
}
@@ -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', () => {
diff --git a/test/nuxt/composables/use-package-transform.spec.ts b/test/nuxt/composables/use-package-transform.spec.ts
index a97e1bc0f8..ba13c3dd5c 100644
--- a/test/nuxt/composables/use-package-transform.spec.ts
+++ b/test/nuxt/composables/use-package-transform.spec.ts
@@ -50,6 +50,16 @@ function createTrustedPublisherWithAttestationsVersion(version: string) {
}
}
+function createStagedPublishVersion(version: string) {
+ return {
+ ...createVersion(version, true),
+ _npmUser: {
+ name: 'publisher',
+ approver: { name: 'approver' },
+ },
+ }
+}
+
function createPackument(
versions: Packument['versions'],
time: Packument['time'],
@@ -80,6 +90,24 @@ function toVersionInfos(packument: ReturnType): Packa
}
describe('transformPackument', () => {
+ it('detects versions released through staged publishing', () => {
+ const packument = createPackument(
+ { '1.0.0': createStagedPublishVersion('1.0.0') },
+ {
+ 'created': '2026-08-03T00:00:00.000Z',
+ 'modified': '2026-08-03T00:00:00.000Z',
+ '1.0.0': '2026-08-03T00:00:00.000Z',
+ },
+ '1.0.0',
+ )
+
+ expect(transformPackument(packument, '1.0.0').versions['1.0.0']?.trustStatus).toEqual({
+ provenance: true,
+ trustedPublisher: false,
+ stagedPublish: true,
+ })
+ })
+
it('includes requested old version and preserves provenance on it', () => {
const packument = createPackument(
{