From 1754700ba4a10ea96f96e749bd36b7cc59b4ff91 Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Wed, 16 Sep 2026 15:30:31 +0900 Subject: [PATCH 1/2] feat(inspector): track trusted publisher and staged publish as supply chain signals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `NpmMeta.provenance` used to be a `'trustedPublisher' | boolean` union. Split it into three independent booleans (`provenance`, `trustedPublisher`, `staged`) and surface them everywhere as one "supply chain score" — how many of the three a package carries. - badge: 3 signals is a green seal, 2 teal, 1 blue, 0 an amber warning; the tooltip spells out which ones are present - grid: the Provenance tab becomes Supply Chain, grouped by score (All 3 Signals / 2 of 3 / 1 of 3 / No Signals) - dependency composition bar: SIGNED/UNSIGNED becomes the same four buckets - `showProvenanceBadge` setting is renamed to `showSupplyChainBadge`; a stored value for the old key is ignored and falls back to the default --- .../app/components/display/PackageSpec.vue | 6 +- .../components/display/ProvenanceBadge.vue | 47 -------------- .../components/display/SupplyChainBadge.vue | 62 +++++++++++++++++++ .../src/app/components/grid/Item.vue | 2 +- .../app/components/panel/PackageDetails.vue | 6 +- .../components/panel/PackageDetailsInfo.vue | 8 +-- .../src/app/components/panel/Settings.vue | 4 +- .../src/app/components/tree/Item.vue | 2 +- .../components/ui/PercentageProvenance.vue | 48 -------------- .../components/ui/PercentageSupplyChain.vue | 53 ++++++++++++++++ .../src/app/pages/grid/[...grid].vue | 44 ++++++------- .../src/app/state/settings.ts | 2 +- .../src/app/utils/supply-chain.ts | 22 +++++++ .../src/shared/types.ts | 2 +- .../src/shared/version-info.ts | 2 + packages/node-modules-tools/src/types/node.ts | 4 +- 16 files changed, 179 insertions(+), 135 deletions(-) delete mode 100644 packages/node-modules-inspector/src/app/components/display/ProvenanceBadge.vue create mode 100644 packages/node-modules-inspector/src/app/components/display/SupplyChainBadge.vue delete mode 100644 packages/node-modules-inspector/src/app/components/ui/PercentageProvenance.vue create mode 100644 packages/node-modules-inspector/src/app/components/ui/PercentageSupplyChain.vue create mode 100644 packages/node-modules-inspector/src/app/utils/supply-chain.ts diff --git a/packages/node-modules-inspector/src/app/components/display/PackageSpec.vue b/packages/node-modules-inspector/src/app/components/display/PackageSpec.vue index 250bbc33..bec69079 100644 --- a/packages/node-modules-inspector/src/app/components/display/PackageSpec.vue +++ b/packages/node-modules-inspector/src/app/components/display/PackageSpec.vue @@ -4,13 +4,13 @@ import DisplayVersion from '@antfu/design/components/Display/DisplayVersion.vue' import { computed } from 'vue' import { getDeprecatedInfo, getVulnerability } from '../../state/payload' import DisplayPackageName from './PackageName.vue' -import DisplayProvenanceBadge from './ProvenanceBadge.vue' +import DisplaySupplyChainBadge from './SupplyChainBadge.vue' const props = defineProps<{ pkg: PackageNode versionSize?: 'small' | 'normal' versionPrefix?: string - showProvenance?: boolean + showSupplyChain?: boolean }>() const deprecation = computed(() => getDeprecatedInfo(props.pkg)) @@ -48,7 +48,7 @@ const vulnerability = computed(() => getVulnerability(props.pkg)) 'text-sm': 'small' === versionSize, }" /> - + diff --git a/packages/node-modules-inspector/src/app/components/display/ProvenanceBadge.vue b/packages/node-modules-inspector/src/app/components/display/ProvenanceBadge.vue deleted file mode 100644 index 3cb87fe6..00000000 --- a/packages/node-modules-inspector/src/app/components/display/ProvenanceBadge.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/packages/node-modules-inspector/src/app/components/display/SupplyChainBadge.vue b/packages/node-modules-inspector/src/app/components/display/SupplyChainBadge.vue new file mode 100644 index 00000000..1630523c --- /dev/null +++ b/packages/node-modules-inspector/src/app/components/display/SupplyChainBadge.vue @@ -0,0 +1,62 @@ + + + diff --git a/packages/node-modules-inspector/src/app/components/grid/Item.vue b/packages/node-modules-inspector/src/app/components/grid/Item.vue index 29bb9815..584bd800 100644 --- a/packages/node-modules-inspector/src/app/components/grid/Item.vue +++ b/packages/node-modules-inspector/src/app/components/grid/Item.vue @@ -25,7 +25,7 @@ defineProps<{ @click="selectedNode = pkg === selectedNode ? undefined : pkg" >
- +
diff --git a/packages/node-modules-inspector/src/app/components/panel/PackageDetails.vue b/packages/node-modules-inspector/src/app/components/panel/PackageDetails.vue index 29dd7305..6b5691f2 100644 --- a/packages/node-modules-inspector/src/app/components/panel/PackageDetails.vue +++ b/packages/node-modules-inspector/src/app/components/panel/PackageDetails.vue @@ -18,7 +18,7 @@ import IntegrationsPublintCounts from '../integrations/PublintCounts.vue' import TreeDependencies from '../tree/Dependencies.vue' import UiPercentageFileCategories from '../ui/PercentageFileCategories.vue' import UiPercentageModuleType from '../ui/PercentageModuleType.vue' -import UiPercentageProvenance from '../ui/PercentageProvenance.vue' +import UiPercentageSupplyChain from '../ui/PercentageSupplyChain.vue' import PanelMaintainerActionTypePill from './MaintainerActionTypePill.vue' import PanelPackageDetailsInfo from './PackageDetailsInfo.vue' @@ -466,9 +466,9 @@ const thirdPartyServices = computed(() => { :flat="settings.deepDependenciesTree" />
- Dependency Provenance + Dependency Supply Chain
- diff --git a/packages/node-modules-inspector/src/app/components/panel/PackageDetailsInfo.vue b/packages/node-modules-inspector/src/app/components/panel/PackageDetailsInfo.vue index dcaa98ea..3c54ef3e 100644 --- a/packages/node-modules-inspector/src/app/components/panel/PackageDetailsInfo.vue +++ b/packages/node-modules-inspector/src/app/components/panel/PackageDetailsInfo.vue @@ -8,15 +8,15 @@ import { NuxtLink } from '#components' import { getBackend } from '../../backends' import { selectedNode } from '../../state/current' import { filters } from '../../state/filters' -import { getDeprecatedInfo, getNpmMeta, getNpmMetaLatest, getPublishTime, payloads } from '../../state/payload' +import { getDeprecatedInfo, getNpmMetaLatest, getPublishTime, payloads } from '../../state/payload' import { settings } from '../../state/settings' import DisplayAuthors from '../display/Authors.vue' import DisplayDateBadge from '../display/DateBadge.vue' import DisplayModuleType from '../display/ModuleType' import DisplayNodeVersionRange from '../display/NodeVersionRange.vue' import DisplayPackageName from '../display/PackageName.vue' -import DisplayProvenanceBadge from '../display/ProvenanceBadge.vue' import DisplaySourceTypeBadge from '../display/SourceTypeBadge.vue' +import DisplaySupplyChainBadge from '../display/SupplyChainBadge.vue' import DisplayVersionWithUpdates from '../display/VersionWithUpdates.vue' import PanelPackageFunding from './PackageFunding.vue' @@ -27,7 +27,6 @@ const props = defineProps<{ const backend = getBackend() const router = useRouter() -const meta = computed(() => getNpmMeta(props.pkg)) const latestMeta = computed(() => getNpmMetaLatest(props.pkg)) const deprecation = computed(() => getDeprecatedInfo(props.pkg)) const resolved = computed(() => props.pkg.resolved) @@ -55,12 +54,11 @@ function showDuplicatedGraph(pkgs: PackageNode[]) {
diff --git a/packages/node-modules-inspector/src/app/components/panel/Settings.vue b/packages/node-modules-inspector/src/app/components/panel/Settings.vue index 9cf27333..5b78099b 100644 --- a/packages/node-modules-inspector/src/app/components/panel/Settings.vue +++ b/packages/node-modules-inspector/src/app/components/panel/Settings.vue @@ -36,9 +36,9 @@ const showConfigDialog = ref(false) - + diff --git a/packages/node-modules-inspector/src/app/components/tree/Item.vue b/packages/node-modules-inspector/src/app/components/tree/Item.vue index 7b3d8255..2c10cd78 100644 --- a/packages/node-modules-inspector/src/app/components/tree/Item.vue +++ b/packages/node-modules-inspector/src/app/components/tree/Item.vue @@ -29,7 +29,7 @@ withDefaults( - + diff --git a/packages/node-modules-inspector/src/app/components/ui/PercentageProvenance.vue b/packages/node-modules-inspector/src/app/components/ui/PercentageProvenance.vue deleted file mode 100644 index 77f0144e..00000000 --- a/packages/node-modules-inspector/src/app/components/ui/PercentageProvenance.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/packages/node-modules-inspector/src/app/components/ui/PercentageSupplyChain.vue b/packages/node-modules-inspector/src/app/components/ui/PercentageSupplyChain.vue new file mode 100644 index 00000000..dd5a8749 --- /dev/null +++ b/packages/node-modules-inspector/src/app/components/ui/PercentageSupplyChain.vue @@ -0,0 +1,53 @@ + + + diff --git a/packages/node-modules-inspector/src/app/pages/grid/[...grid].vue b/packages/node-modules-inspector/src/app/pages/grid/[...grid].vue index 672c6c70..3b031aac 100644 --- a/packages/node-modules-inspector/src/app/pages/grid/[...grid].vue +++ b/packages/node-modules-inspector/src/app/pages/grid/[...grid].vue @@ -11,9 +11,10 @@ import DisplayModuleType from '../../components/display/ModuleType' import GridExpand from '../../components/grid/Expand.vue' import { getNpmMeta, payloads } from '../../state/payload' import { getModuleType } from '../../utils/module-type' +import { getSupplyChainScore, SUPPLY_CHAIN_SIGNALS } from '../../utils/supply-chain' const params = useRoute().params as Record -const tab = computed<'depth' | 'clusters' | 'module-type' | 'authors' | 'licenses' | 'provenance'>(() => params.grid?.[0] as any || 'depth') +const tab = computed<'depth' | 'clusters' | 'module-type' | 'authors' | 'licenses' | 'supply-chain'>(() => params.grid?.[0] as any || 'depth') const location = window.location @@ -21,6 +22,7 @@ const MAX_DEPTH = 5 interface Group { name: string + description?: string cluster?: string module?: PackageModuleType author?: ParsedAuthor @@ -110,26 +112,24 @@ const groups = computed(() => { expanded: false, })) } - else if (tab.value === 'provenance') { - const map = new Map<'Trusted Publisher' | 'Provenance' | 'None', PackageNode[]>([ - ['Trusted Publisher', []], - ['Provenance', []], - ['None', []], - ]) - for (const pkg of payloads.filtered.packages) { - const meta = getNpmMeta(pkg) - const provenance = meta?.provenance === 'trustedPublisher' - ? 'Trusted Publisher' - : meta?.provenance === true ? 'Provenance' : 'None' - map.get(provenance)!.push(pkg) - } + else if (tab.value === 'supply-chain') { + // Grouped by how many of the three signals a package has, best first. Only + // the top and bottom groups are a single combination, so the others are + // named by count and spelled out in the tooltip. + const map = new Map([[3, []], [2, []], [1, []], [0, []]]) + for (const pkg of payloads.filtered.packages) + map.get(getSupplyChainScore(getNpmMeta(pkg)))!.push(pkg) return [...map.entries()] - .map(([provenance, packages]) => ({ - name: provenance, - provenance, + .map(([score, packages]) => ({ + name: score === 3 + ? `All ${SUPPLY_CHAIN_SIGNALS.length} Signals` + : score === 0 ? 'No Signals' : `${score} of ${SUPPLY_CHAIN_SIGNALS.length} Signals`, + description: score === 3 + ? SUPPLY_CHAIN_SIGNALS.join(' + ') + : `${score === 0 ? 'None' : `Any ${score}`} of: ${SUPPLY_CHAIN_SIGNALS.join(', ')}`, packages, - expanded: provenance !== 'None', + expanded: score > 0, })) } else { @@ -187,9 +187,9 @@ const groups = computed(() => {
License - -
- Provenance + +
+ Supply Chain
@@ -204,7 +204,7 @@ const groups = computed(() => { - {{ group.name }} + {{ group.name }}
diff --git a/packages/node-modules-inspector/src/app/state/settings.ts b/packages/node-modules-inspector/src/app/state/settings.ts index f8a0489c..0531872e 100644 --- a/packages/node-modules-inspector/src/app/state/settings.ts +++ b/packages/node-modules-inspector/src/app/state/settings.ts @@ -11,7 +11,7 @@ export const SETTINGS_DEFAULT: SettingsOptions = { colorizePackageSize: true, showInstallSizeBadge: true, showPublishTimeBadge: false, - showProvenanceBadge: 'present', + showSupplyChainBadge: 'present', showFileComposition: false, showDependencySourceBadge: 'dev', showPublintMessages: false, diff --git a/packages/node-modules-inspector/src/app/utils/supply-chain.ts b/packages/node-modules-inspector/src/app/utils/supply-chain.ts new file mode 100644 index 00000000..d6a6fecd --- /dev/null +++ b/packages/node-modules-inspector/src/app/utils/supply-chain.ts @@ -0,0 +1,22 @@ +import type { NpmMeta } from 'node-modules-tools' + +/** + * The three supply-chain signals npm exposes, in the order they are counted. + */ +export const SUPPLY_CHAIN_SIGNALS = ['Provenance', 'Trusted Publisher', 'Staged Publish'] + +/** + * How many of the three supply-chain signals (provenance attestation, trusted + * publisher, staged publish) a package carries. `0` means none of them, or that + * there is no npm meta for it at all. + */ +export function getSupplyChainScore(meta: NpmMeta | null | undefined): number { + let score = 0 + if (meta?.provenance) + score++ + if (meta?.trustedPublisher) + score++ + if (meta?.staged) + score++ + return score +} diff --git a/packages/node-modules-inspector/src/shared/types.ts b/packages/node-modules-inspector/src/shared/types.ts index 2d492190..a6a97000 100644 --- a/packages/node-modules-inspector/src/shared/types.ts +++ b/packages/node-modules-inspector/src/shared/types.ts @@ -80,7 +80,7 @@ export interface SettingsOptions { colorizePackageSize: boolean showInstallSizeBadge: boolean showPublishTimeBadge: boolean - showProvenanceBadge: 'present' | 'absent' | 'none' + showSupplyChainBadge: 'present' | 'absent' | 'none' showFileComposition: boolean showDependencySourceBadge: 'none' | 'dev' | 'prod' | 'both' treatFauxAsESM: boolean diff --git a/packages/node-modules-inspector/src/shared/version-info.ts b/packages/node-modules-inspector/src/shared/version-info.ts index 14560f92..6cd7d9db 100644 --- a/packages/node-modules-inspector/src/shared/version-info.ts +++ b/packages/node-modules-inspector/src/shared/version-info.ts @@ -142,6 +142,8 @@ export async function getPackagesNpmMetaLatest( deprecated: r.deprecated, version: r.version!, provenance: r.provenance, + trustedPublisher: r.trustedPublisher, + staged: r.staged, fetechedAt: Date.now(), vaildUntil: Date.now() + ttl, } diff --git a/packages/node-modules-tools/src/types/node.ts b/packages/node-modules-tools/src/types/node.ts index 4e676dbb..4ed6638f 100644 --- a/packages/node-modules-tools/src/types/node.ts +++ b/packages/node-modules-tools/src/types/node.ts @@ -97,7 +97,9 @@ export type AuditLevelString = 'low' | 'moderate' | 'high' | 'critical' export interface NpmMeta { publishedAt: number deprecated?: string - provenance?: 'trustedPublisher' | boolean + provenance?: boolean + trustedPublisher?: boolean + staged?: boolean vulnerability?: { level: AuditLevelString title: string From 08f9f79db0202064944940aedf4ae281b75173f2 Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Wed, 16 Sep 2026 15:33:55 +0900 Subject: [PATCH 2/2] fix type error --- .../node-modules-inspector/src/app/webcontainer/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node-modules-inspector/src/app/webcontainer/constants.ts b/packages/node-modules-inspector/src/app/webcontainer/constants.ts index 12db0496..87b334f3 100644 --- a/packages/node-modules-inspector/src/app/webcontainer/constants.ts +++ b/packages/node-modules-inspector/src/app/webcontainer/constants.ts @@ -1,4 +1,3 @@ -// @ts-expect-error missing types import { WEBCONTAINER_SERVER_CODE as _WEBCONTAINER_SERVER_CODE } from '#build/webcontainer-server-code' export const CODE_SERVER = _WEBCONTAINER_SERVER_CODE as string