Skip to content

Commit 9bc1b3b

Browse files
committed
fix: rank trusted publishing higher and update wording
1 parent 504da16 commit 9bc1b3b

File tree

8 files changed

+181
-52
lines changed

8 files changed

+181
-52
lines changed

app/pages/package/[[org]]/[name].vue

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,25 +1054,43 @@ onKeyStroke(
10541054
{{ $t('package.security_downgrade.title') }}
10551055
</h3>
10561056
<p class="mt-2 mb-0 text-sm">
1057-
{{ $t('package.security_downgrade.description') }}
1058-
</p>
1059-
<p v-if="publishSecurityDowngrade.trustedVersion" class="mt-2 mb-0 text-sm">
1060-
{{
1061-
$t('package.security_downgrade.fallback_install', {
1062-
version: publishSecurityDowngrade.trustedVersion,
1063-
})
1064-
}}
1065-
</p>
1066-
<p class="mt-2 mb-0 text-sm">
1067-
<a
1068-
href="https://docs.npmjs.com/generating-provenance-statements"
1069-
target="_blank"
1070-
rel="noopener noreferrer"
1071-
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"
1057+
<i18n-t
1058+
:keypath="`package.security_downgrade.description_to_${publishSecurityDowngrade.downgradedTrustLevel}_${publishSecurityDowngrade.trustedTrustLevel}`"
1059+
tag="span"
1060+
scope="global"
10721061
>
1073-
{{ $t('package.security_downgrade.learn_more') }}
1074-
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
1075-
</a>
1062+
<template #provenance>
1063+
<a
1064+
href="https://docs.npmjs.com/generating-provenance-statements"
1065+
target="_blank"
1066+
rel="noopener noreferrer"
1067+
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"
1068+
>{{ $t('package.security_downgrade.provenance_link_text')
1069+
}}<span class="i-carbon-launch w-3 h-3" aria-hidden="true"
1070+
/></a>
1071+
</template>
1072+
<template #trustedPublishing>
1073+
<a
1074+
href="https://docs.npmjs.com/adding-a-trusted-publisher-to-a-package"
1075+
target="_blank"
1076+
rel="noopener noreferrer"
1077+
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"
1078+
>{{ $t('package.security_downgrade.trusted_publishing_link_text')
1079+
}}<span class="i-carbon-launch w-3 h-3" aria-hidden="true"
1080+
/></a>
1081+
</template>
1082+
</i18n-t>
1083+
{{ ' ' }}
1084+
<template v-if="publishSecurityDowngrade.trustedVersion">
1085+
{{
1086+
$t(
1087+
`package.security_downgrade.fallback_install_${publishSecurityDowngrade.trustedTrustLevel}`,
1088+
{
1089+
version: publishSecurityDowngrade.trustedVersion,
1090+
},
1091+
)
1092+
}}
1093+
</template>
10761094
</p>
10771095
</div>
10781096
<TerminalInstall

app/utils/publish-security.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,31 @@ import { compare, major } from 'semver'
44
export interface PublishSecurityDowngrade {
55
downgradedVersion: string
66
downgradedPublishedAt?: string
7+
downgradedTrustLevel: PublishTrustLevel
78
/** Recommended trusted version within the same major, if one exists */
89
trustedVersion?: string
910
trustedPublishedAt?: string
11+
trustedTrustLevel: PublishTrustLevel
1012
}
1113

1214
type VersionWithIndex = PackageVersionInfo & {
1315
index: number
1416
timestamp: number
1517
trustRank: number
18+
resolvedTrustLevel: PublishTrustLevel
1619
}
1720

1821
const TRUST_RANK: Record<PublishTrustLevel, number> = {
1922
none: 0,
20-
trustedPublisher: 1,
21-
provenance: 2,
23+
provenance: 1,
24+
trustedPublisher: 2,
2225
}
2326

24-
function getTrustRank(version: PackageVersionInfo): number {
25-
if (version.trustLevel) return TRUST_RANK[version.trustLevel]
27+
function resolveTrustLevel(version: PackageVersionInfo): PublishTrustLevel {
28+
if (version.trustLevel) return version.trustLevel
2629
// Fallback for legacy data: hasProvenance only indicates non-'none' trust,
27-
// so map it to trustedPublisher (the lower rank) to avoid over-ranking
28-
return version.hasProvenance ? TRUST_RANK.trustedPublisher : TRUST_RANK.none
30+
// so map it to provenance (the lower rank) to avoid over-ranking
31+
return version.hasProvenance ? 'provenance' : 'none'
2932
}
3033

3134
function toTimestamp(time?: string): number {
@@ -65,12 +68,16 @@ export function detectPublishSecurityDowngradeForVersion(
6568
if (versions.length < 2 || !viewedVersion) return null
6669

6770
const sorted = versions
68-
.map((version, index) => ({
69-
...version,
70-
index,
71-
timestamp: toTimestamp(version.time),
72-
trustRank: getTrustRank(version),
73-
}))
71+
.map((version, index) => {
72+
const resolvedTrustLevel = resolveTrustLevel(version)
73+
return {
74+
...version,
75+
index,
76+
timestamp: toTimestamp(version.time),
77+
trustRank: TRUST_RANK[resolvedTrustLevel],
78+
resolvedTrustLevel,
79+
}
80+
})
7481
.sort(sortByRecency)
7582

7683
const currentIndex = sorted.findIndex(version => version.version === viewedVersion)
@@ -108,7 +115,9 @@ export function detectPublishSecurityDowngradeForVersion(
108115
return {
109116
downgradedVersion: current.version,
110117
downgradedPublishedAt: current.time,
118+
downgradedTrustLevel: current.resolvedTrustLevel,
111119
trustedVersion: recommendation?.version,
112120
trustedPublishedAt: recommendation?.time,
121+
trustedTrustLevel: strongestOlder.resolvedTrustLevel,
113122
}
114123
}

i18n/locales/en.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,14 @@
257257
"error_loading": "Failed to load provenance details"
258258
},
259259
"security_downgrade": {
260-
"title": "Provenance downgrade",
261-
"description": "An older version of this package was published with provenance or trusted publishing. This version was published without it.",
262-
"fallback_install": "Install commands below are pinned to version {version}, which was published with a stronger trust method.",
263-
"learn_more": "Learn more about provenance"
260+
"title": "Trust downgrade",
261+
"description_to_none_provenance": "This version was published without {provenance}.",
262+
"description_to_none_trustedPublisher": "This version was published without {trustedPublishing}.",
263+
"description_to_provenance_trustedPublisher": "This version uses {provenance} but not {trustedPublishing}.",
264+
"fallback_install_provenance": "Install commands are pinned to {version}, the last version with provenance.",
265+
"fallback_install_trustedPublisher": "Install commands are pinned to {version}, the last version with trusted publishing.",
266+
"provenance_link_text": "provenance",
267+
"trusted_publishing_link_text": "trusted publishing"
264268
},
265269
"keywords_title": "Keywords",
266270
"compatibility": "Compatibility",

i18n/schema.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,36 @@
772772
},
773773
"additionalProperties": false
774774
},
775+
"security_downgrade": {
776+
"type": "object",
777+
"properties": {
778+
"title": {
779+
"type": "string"
780+
},
781+
"description_to_none_provenance": {
782+
"type": "string"
783+
},
784+
"description_to_none_trustedPublisher": {
785+
"type": "string"
786+
},
787+
"description_to_provenance_trustedPublisher": {
788+
"type": "string"
789+
},
790+
"fallback_install_provenance": {
791+
"type": "string"
792+
},
793+
"fallback_install_trustedPublisher": {
794+
"type": "string"
795+
},
796+
"provenance_link_text": {
797+
"type": "string"
798+
},
799+
"trusted_publishing_link_text": {
800+
"type": "string"
801+
}
802+
},
803+
"additionalProperties": false
804+
},
775805
"keywords_title": {
776806
"type": "string"
777807
},

lunaria/files/en-GB.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,14 @@
256256
"error_loading": "Failed to load provenance details"
257257
},
258258
"security_downgrade": {
259-
"title": "Provenance downgrade",
260-
"description": "An older version of this package was published with provenance or trusted publishing. This version was published without it.",
261-
"fallback_install": "Install commands below are pinned to version {version}, which was published with a stronger trust method.",
262-
"learn_more": "Learn more about provenance"
259+
"title": "Trust downgrade",
260+
"description_to_none_provenance": "This version was published without {provenance}.",
261+
"description_to_none_trustedPublisher": "This version was published without {trustedPublishing}.",
262+
"description_to_provenance_trustedPublisher": "This version uses {provenance} but not {trustedPublishing}.",
263+
"fallback_install_provenance": "Install commands are pinned to {version}, the last version with provenance.",
264+
"fallback_install_trustedPublisher": "Install commands are pinned to {version}, the last version with trusted publishing.",
265+
"provenance_link_text": "provenance",
266+
"trusted_publishing_link_text": "trusted publishing"
263267
},
264268
"keywords_title": "Keywords",
265269
"compatibility": "Compatibility",

lunaria/files/en-US.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,14 @@
256256
"error_loading": "Failed to load provenance details"
257257
},
258258
"security_downgrade": {
259-
"title": "Provenance downgrade",
260-
"description": "An older version of this package was published with provenance or trusted publishing. This version was published without it.",
261-
"fallback_install": "Install commands below are pinned to version {version}, which was published with a stronger trust method.",
262-
"learn_more": "Learn more about provenance"
259+
"title": "Trust downgrade",
260+
"description_to_none_provenance": "This version was published without {provenance}.",
261+
"description_to_none_trustedPublisher": "This version was published without {trustedPublishing}.",
262+
"description_to_provenance_trustedPublisher": "This version uses {provenance} but not {trustedPublishing}.",
263+
"fallback_install_provenance": "Install commands are pinned to {version}, the last version with provenance.",
264+
"fallback_install_trustedPublisher": "Install commands are pinned to {version}, the last version with trusted publishing.",
265+
"provenance_link_text": "provenance",
266+
"trusted_publishing_link_text": "trusted publishing"
263267
},
264268
"keywords_title": "Keywords",
265269
"compatibility": "Compatibility",

test/nuxt/composables/use-package-transform.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,10 @@ describe('transformPackument', () => {
182182
expect(detectPublishSecurityDowngradeForVersion(infos, '1.0.1')).toEqual({
183183
downgradedVersion: '1.0.1',
184184
downgradedPublishedAt: '2026-01-02T00:00:00.000Z',
185+
downgradedTrustLevel: 'none',
185186
trustedVersion: '1.0.0',
186187
trustedPublishedAt: '2026-01-01T00:00:00.000Z',
188+
trustedTrustLevel: 'provenance',
187189
})
188190
})
189191

test/unit/app/utils/publish-security.spec.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,27 @@ describe('detectPublishSecurityDowngradeForVersion', () => {
3030
expect(result).toEqual({
3131
downgradedVersion: '1.0.1',
3232
downgradedPublishedAt: '2026-01-02T00:00:00.000Z',
33+
downgradedTrustLevel: 'none',
3334
trustedVersion: '1.0.0',
3435
trustedPublishedAt: '2026-01-01T00:00:00.000Z',
36+
trustedTrustLevel: 'provenance',
3537
})
3638
})
3739

38-
it('flags trust downgrade from provenance to trustedPublisher', () => {
40+
it('flags trust downgrade from trustedPublisher to provenance', () => {
3941
const result = detectPublishSecurityDowngradeForVersion(
4042
[
4143
{
4244
version: '1.0.0',
4345
time: '2026-01-01T00:00:00.000Z',
4446
hasProvenance: true,
45-
trustLevel: 'provenance',
47+
trustLevel: 'trustedPublisher',
4648
},
4749
{
4850
version: '1.0.1',
4951
time: '2026-01-02T00:00:00.000Z',
5052
hasProvenance: true,
51-
trustLevel: 'trustedPublisher',
53+
trustLevel: 'provenance',
5254
},
5355
],
5456
'1.0.1',
@@ -57,11 +59,35 @@ describe('detectPublishSecurityDowngradeForVersion', () => {
5759
expect(result).toEqual({
5860
downgradedVersion: '1.0.1',
5961
downgradedPublishedAt: '2026-01-02T00:00:00.000Z',
62+
downgradedTrustLevel: 'provenance',
6063
trustedVersion: '1.0.0',
6164
trustedPublishedAt: '2026-01-01T00:00:00.000Z',
65+
trustedTrustLevel: 'trustedPublisher',
6266
})
6367
})
6468

69+
it('does not flag upgrade from provenance to trustedPublisher', () => {
70+
const result = detectPublishSecurityDowngradeForVersion(
71+
[
72+
{
73+
version: '1.0.0',
74+
time: '2026-01-01T00:00:00.000Z',
75+
hasProvenance: true,
76+
trustLevel: 'provenance',
77+
},
78+
{
79+
version: '1.0.1',
80+
time: '2026-01-02T00:00:00.000Z',
81+
hasProvenance: true,
82+
trustLevel: 'trustedPublisher',
83+
},
84+
],
85+
'1.0.1',
86+
)
87+
88+
expect(result).toBeNull()
89+
})
90+
6591
it('flags ongoing downgraded versions until an upgrade happens', () => {
6692
const versions = [
6793
{
@@ -216,29 +242,61 @@ describe('detectPublishSecurityDowngradeForVersion', () => {
216242
expect(result?.trustedVersion).toBe('2.0.0')
217243
})
218244

219-
it('uses trustedPublisher rank (not provenance) for hasProvenance fallback without trustLevel', () => {
220-
// When trustLevel is absent, hasProvenance: true should map to trustedPublisher rank,
221-
// not provenance rank. This means a version with only hasProvenance: true should NOT
222-
// be considered a downgrade from trustedPublisher.
245+
it('uses provenance rank (not trustedPublisher) for hasProvenance fallback without trustLevel', () => {
246+
// When trustLevel is absent, hasProvenance: true should map to provenance rank,
247+
// not trustedPublisher rank. This means a version with only hasProvenance: true
248+
// should be considered a downgrade from trustedPublisher.
249+
const result = detectPublishSecurityDowngradeForVersion(
250+
[
251+
{
252+
version: '1.0.0',
253+
time: '2026-01-01T00:00:00.000Z',
254+
hasProvenance: true,
255+
trustLevel: 'trustedPublisher',
256+
},
257+
{
258+
version: '1.0.1',
259+
time: '2026-01-02T00:00:00.000Z',
260+
hasProvenance: true,
261+
// no trustLevel — fallback path maps to provenance
262+
},
263+
],
264+
'1.0.1',
265+
)
266+
267+
// hasProvenance fallback maps to provenance (rank 1), trustedPublisher is rank 2, so this is a downgrade
268+
expect(result).toEqual({
269+
downgradedVersion: '1.0.1',
270+
downgradedPublishedAt: '2026-01-02T00:00:00.000Z',
271+
downgradedTrustLevel: 'provenance',
272+
trustedVersion: '1.0.0',
273+
trustedPublishedAt: '2026-01-01T00:00:00.000Z',
274+
trustedTrustLevel: 'trustedPublisher',
275+
})
276+
})
277+
278+
it('does not flag hasProvenance fallback against provenance trustLevel', () => {
279+
// When trustLevel is absent, hasProvenance: true maps to provenance rank.
280+
// An explicit provenance trustLevel is the same rank, so no downgrade.
223281
const result = detectPublishSecurityDowngradeForVersion(
224282
[
225283
{
226284
version: '1.0.0',
227285
time: '2026-01-01T00:00:00.000Z',
228286
hasProvenance: true,
229-
// no trustLevel — fallback path
287+
// no trustLevel — fallback path maps to provenance
230288
},
231289
{
232290
version: '1.0.1',
233291
time: '2026-01-02T00:00:00.000Z',
234292
hasProvenance: true,
235-
trustLevel: 'trustedPublisher',
293+
trustLevel: 'provenance',
236294
},
237295
],
238296
'1.0.1',
239297
)
240298

241-
// Both should be treated as trustedPublisher rank, so no downgrade
299+
// Both are provenance rank, so no downgrade
242300
expect(result).toBeNull()
243301
})
244302
})

0 commit comments

Comments
 (0)