Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
91d3b99
feat: add `AppPopover` component
AscaL Jan 30, 2026
c9cc109
feat(constants): add error constant for provenance fetch failure
AscaL Jan 30, 2026
c19f8ef
feat(i18n): add provenance section to english
AscaL Jan 30, 2026
4b142f4
feat: add `PackageProvenanceSection` component
AscaL Jan 30, 2026
64f9601
feat(types): add `ProvenanceDetails` type
AscaL Jan 30, 2026
cc60590
feat(provenance): add provenance utility functions
AscaL Jan 30, 2026
0927569
style(PackageProvenanceSection): add mt spacing
AscaL Jan 30, 2026
2c99239
feat(provenance): add api endpoint for provenance details
AscaL Jan 30, 2026
e6bb3e2
feat(package): add provenance and popover to package
AscaL Jan 30, 2026
723ee08
docs(README): update provenance description
AscaL Jan 30, 2026
d9408b5
Merge remote-tracking branch 'origin' into feature/provenance
AscaL Jan 30, 2026
9c1c0fd
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 30, 2026
86530fc
fix(AppPopover): a11y issues
AscaL Jan 30, 2026
1df3da5
fix(provenance): a11y and hydration issues
AscaL Jan 30, 2026
0ce8e20
fix(Popover): use shallowref
AscaL Jan 31, 2026
4fe71b7
fix: use shallowRef
AscaL Jan 31, 2026
558f331
feat(AppPopover): timeout not reactive
AscaL Jan 31, 2026
0c65073
fix(AppPopover): remove .value for timeout
AscaL Jan 31, 2026
dc02155
style(PackageProvenanceSection): improve styling for provenance section
AscaL Jan 31, 2026
b5d0e08
Merge branch 'feature/provenance' of https://github.com/AscaL/npmx.de…
AscaL Jan 31, 2026
aa5c739
Merge branch 'main' into feature/provenance
AscaL Jan 31, 2026
0490129
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 31, 2026
73169c0
Merge branch 'main' into feature/provenance
AscaL Feb 1, 2026
b9fd47e
merge: resolve conflicts with main
danielroe Feb 1, 2026
f95b9cb
Merge remote-tracking branch 'origin/main' into feature/provenance
danielroe Feb 1, 2026
121ebeb
chore: merge remote-tracking branch 'origin' into feature/provenance
AscaL Feb 2, 2026
8b37de5
Merge branch 'npmx-dev:main' into feature/provenance
AscaL Feb 2, 2026
4df6684
test(a11y): add accessibility tests for AppPopover and PackageProvena…
AscaL Feb 2, 2026
49152ff
feat(package): add back provenance popover
AscaL Feb 2, 2026
23582b9
fix(PackageProvenanceSection): remove link to npm and props
AscaL Feb 2, 2026
c5e939e
chore: merge remote-tracking branch 'origin' into feature/provenance
AscaL Feb 2, 2026
a7b1e22
Merge branch 'main' into feature/provenance
AscaL Feb 2, 2026
fed4af5
feat(a11y): add accessible label to AppPopover
AscaL Feb 2, 2026
b632fe3
chore: merge branch 'feature/provenance' of https://github.com/AscaL/…
AscaL Feb 2, 2026
7c1e4de
Merge branch 'main' into feature/provenance
AscaL Feb 2, 2026
2e6a643
Merge branch 'main' into feature/provenance
AscaL Feb 3, 2026
1495782
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 3, 2026
880b091
feat: use tooltip for provenance badge
AscaL Feb 3, 2026
392c73e
refactor: update provenance section with i18n keypath
AscaL Feb 3, 2026
fa236e6
fix: 404 on missing pkg version
AscaL Feb 3, 2026
697a85b
chore: merge remote-tracking branch 'origin' into feature/provenance
AscaL Feb 4, 2026
38f4d00
Merge branch 'main' into feature/provenance
AscaL Feb 4, 2026
2c32786
Merge remote-tracking branch 'origin/main' into feature/provenance
danielroe Feb 4, 2026
c77a400
fix: update icons
danielroe Feb 4, 2026
4f22aea
fix: error handling
danielroe Feb 4, 2026
43bb79a
fix: also handle refs/tags publishing
danielroe Feb 4, 2026
1175730
fix: cache provenance permanently
danielroe Feb 4, 2026
81a644c
fix: merge conflict
danielroe Feb 4, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the n
- **Fast search** – quick package search with instant results
- **Package details** – READMEs, versions, dependencies, and metadata
- **Code viewer** – browse package source code with syntax highlighting and permalink to specific lines
- **Provenance indicators** – verified build badges for packages with npm provenance
- **Provenance indicators** – verified build badges and provenance section below the README
- **Multi-provider repository support** – stars/forks from GitHub, GitLab, Bitbucket, Codeberg, Gitee, Sourcehut, Forgejo, Gitea, Radicle, and Tangled
- **JSR availability** – see if scoped packages are also available on JSR
- **Package badges** – module format (ESM/CJS/dual), TypeScript types (with `@types/*` links), and engine constraints
Expand Down
76 changes: 76 additions & 0 deletions app/components/AppPopover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup lang="ts">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this something that TooltipApp would help us with?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that seems to be done for simple tooltips
Image
while this one has a link inside. I could remove the link and keep the scroll to the #provenance section by clicking the badge if you think that's better?

Image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, let's make it a tooltip that just says 'Built and signed on GitHub Actions' (no icon needed, if it's displaying by hovering an icon)

or ... an alternative idea might be to display this as a banner above the readme (like the vulnerability banner) which is expandable with details (and remove the shield from next to the version). that might be a way of freeing up space in this very top area, but it would push the readme further down by a line.

what do you think?

@AscaL AscaL Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd just make it simpler, I think readmes are generally cramped as is, and i am not sure how many people know about provenance... I honestly didn't know it existed till 3 days ago. In my head vulns are much more important and i don't think they deserve the same "space". I guess that is just my personal experience talking.
Plus i think the badge there is just easy to look for, and if it's there you "know".

const props = defineProps<{
/** Position of the popover panel: 'top' | 'bottom' | 'left' | 'right' */
position?: 'top' | 'bottom' | 'left' | 'right'
}>()

const isOpen = shallowRef(false)
const popoverId = useId()
let closeTimeout: NodeJS.Timeout | null = null

const closeDelayMs = 500

// Panel placement relative to trigger
const panelPositionClasses: Record<string, string> = {
top: 'bottom-full left-1/2 -translate-x-1/2',
bottom: 'top-full left-1/2 -translate-x-1/2',
left: 'right-full top-1/2 -translate-y-1/2',
right: 'left-full top-1/2 -translate-y-1/2',
}

const panelPosition = computed(() => panelPositionClasses[props.position ?? 'bottom'])

function clearCloseTimeout() {
if (closeTimeout !== null) {
clearTimeout(closeTimeout)
closeTimeout = null
}
}

function open() {
clearCloseTimeout()
isOpen.value = true
}

function close() {
clearCloseTimeout()
closeTimeout = setTimeout(() => {
closeTimeout = null
isOpen.value = false
}, closeDelayMs)
}

onBeforeUnmount(clearCloseTimeout)
</script>

<template>
<div
class="relative inline-flex"
@mouseenter="open"
@mouseleave="close"
@focusin="open"
@focusout="close"
>
<slot :popover-visible="isOpen" :popover-id="popoverId" />

<Transition
enter-active-class="transition-opacity duration-150 motion-reduce:transition-none"
leave-active-class="transition-opacity duration-100 motion-reduce:transition-none"
enter-from-class="opacity-0"
leave-to-class="opacity-0"
>
<div
v-if="isOpen"
:id="popoverId"
role="dialog"
aria-modal="false"
class="absolute font-mono text-xs text-fg bg-bg-subtle border border-border rounded-lg shadow-lg z-[100] pointer-events-auto px-4 py-3 min-w-[14rem] max-w-[22rem] whitespace-normal"
:class="panelPosition"
@mouseenter="open"
@mouseleave="close"
>
<slot name="content" />
</div>
</Transition>
</div>
</template>
116 changes: 116 additions & 0 deletions app/components/PackageProvenanceSection.vue
Comment thread
AscaL marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<script setup lang="ts">
import type { ProvenanceDetails } from '#shared/types'

defineProps<{
/** Parsed provenance details from the API */
details: ProvenanceDetails
/** Optional: link "View on npm" to package provenance page */
packageName?: string
version?: string
}>()
</script>

<template>
<section id="provenance" aria-labelledby="provenance-heading" class="scroll-mt-20">
<h2 id="provenance-heading" class="group text-xs text-fg-subtle uppercase tracking-wider mb-3">
<a
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
href="#provenance"
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
>
{{ $t('package.provenance_section.title') }}
<span
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
aria-hidden="true"
/>
</a>
</h2>

<div class="space-y-3 border border-border rounded-lg p-5">
<p class="flex items-center gap-2 text-sm text-fg m-0">
<span
class="i-solar-shield-check-outline w-4 h-4 shrink-0 text-emerald-500"
aria-hidden="true"
/>
<span
v-html="
$t('package.provenance_section.built_and_signed_on', {
provider: `<b>${details.providerLabel}</b>`,
})
"
/>
</p>
<a
v-if="details.buildSummaryUrl"
:href="details.buildSummaryUrl"
target="_blank"
rel="noopener noreferrer"
class="link text-sm text-fg-muted block mt-1"
>
{{ $t('package.provenance_section.view_build_summary') }}
</a>

<dl class="m-0 mt-4 flex justify-between">
<div v-if="details.sourceCommitUrl" class="flex flex-col gap-0.5">
<dt class="font-mono text-xs text-fg-muted m-0">
{{ $t('package.provenance_section.source_commit') }}
</dt>
<dd class="m-0">
<a
:href="details.sourceCommitUrl"
target="_blank"
rel="noopener noreferrer"
class="link font-mono text-sm break-all"
>
{{
details.sourceCommitSha
? `${details.sourceCommitSha.slice(0, 12)}`
: details.sourceCommitUrl
}}
</a>
</dd>
</div>
<div v-if="details.buildFileUrl" class="flex flex-col gap-0.5">
<dt class="font-mono text-xs text-fg-muted m-0">
{{ $t('package.provenance_section.build_file') }}
</dt>
<dd class="m-0">
<a
:href="details.buildFileUrl"
target="_blank"
rel="noopener noreferrer"
class="link font-mono text-sm break-all"
>
{{ details.buildFilePath ?? details.buildFileUrl }}
</a>
</dd>
</div>
<div v-if="details.publicLedgerUrl" class="flex flex-col gap-0.5">
<dt class="font-mono text-xs text-fg-muted m-0">
{{ $t('package.provenance_section.public_ledger') }}
</dt>
<dd class="m-0">
<a
:href="details.publicLedgerUrl"
target="_blank"
rel="noopener noreferrer"
class="link text-sm"
>
{{ $t('package.provenance_section.transparency_log_entry') }}
</a>
</dd>
</div>
</dl>
</div>

<p v-if="packageName && version" class="mt-4 m-0">
<a
:href="`https://www.npmjs.com/package/${packageName}/v/${version}#provenance`"
target="_blank"
rel="noopener noreferrer"
class="link-subtle font-mono text-sm"
>
{{ $t('common.view_on_npm') }}
</a>
Comment thread
AscaL marked this conversation as resolved.
Outdated
</p>
</section>
</template>
Comment thread
AscaL marked this conversation as resolved.
56 changes: 54 additions & 2 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type {
NpmVersionDist,
PackumentVersion,
ProvenanceDetails,
ReadmeResponse,
SkillsListResponse,
} from '#shared/types'
Expand Down Expand Up @@ -141,6 +142,35 @@ const { data: vulnTree, status: vulnTreeStatus } = useDependencyAnalysis(
() => displayVersion.value?.version ?? '',
)

const {
data: provenanceData,
status: provenanceStatus,
execute: fetchProvenance,
} = useLazyFetch<ProvenanceDetails | null>(
() => {
const v = displayVersion.value
if (!v || !hasProvenance(v)) return ''
return `/api/registry/provenance/${packageName.value}/v/${v.version}`
},
{
default: () => null,
server: false,
immediate: false,
},
)
if (import.meta.client) {
watchEffect(() => {
if (displayVersion.value && hasProvenance(displayVersion.value)) {
fetchProvenance()
}
})
}

const provenanceBadgeMounted = shallowRef(false)
onMounted(() => {
provenanceBadgeMounted.value = true
})

// Keep latestVersion for comparison (to show "(latest)" badge)
const latestVersion = computed(() => {
if (!pkg.value) return null
Expand Down Expand Up @@ -294,8 +324,6 @@ function getDependencyCount(version: PackumentVersion | null): number {
return Object.keys(version.dependencies).length
}

// Check if a version has provenance/attestations
// The dist object may have attestations that aren't in the base type
function hasProvenance(version: PackumentVersion | null): boolean {
if (!version?.dist) return false
const dist = version.dist as NpmVersionDist
Expand Down Expand Up @@ -996,6 +1024,30 @@ function handleClick(event: MouseEvent) {
$t('package.readme.view_on_github')
}}</a>
</p>

<section
v-if="hasProvenance(displayVersion) && provenanceBadgeMounted"
id="provenance"
class="scroll-mt-20"
>
<div
v-if="provenanceStatus === 'pending'"
class="mt-8 flex items-center gap-2 text-fg-subtle text-sm"
>
<span
class="i-carbon-circle-dash w-4 h-4 motion-safe:animate-spin"
aria-hidden="true"
/>
<span>{{ $t('package.provenance_section.title') }}…</span>
</div>
<PackageProvenanceSection
v-else-if="provenanceData"
:details="provenanceData"
:package-name="pkg.name"
:version="displayVersion?.version"
class="mt-8"
/>
</section>
</section>

<div class="area-sidebar">
Expand Down
13 changes: 12 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@
"no_readme": "No README available.",
"view_on_github": "View on GitHub"
},
"provenance_section": {
"title": "Provenance",
"built_and_signed_on": "Built and signed on {provider}",
"view_build_summary": "View build summary",
"source_commit": "Source Commit",
"build_file": "Build File",
"public_ledger": "Public Ledger",
"transparency_log_entry": "Transparency log entry",
"view_more_details": "View more details"
},
"keywords_title": "Keywords",
"compatibility": "Compatibility",
"card": {
Expand Down Expand Up @@ -600,7 +610,8 @@
"provenance": {
"verified": "verified",
"verified_title": "Verified provenance",
"verified_via": "Verified: published via {provider}"
"verified_via": "Verified: published via {provider}",
"view_more_details": "View more details"
},
"jsr": {
"title": "also available on JSR",
Expand Down
13 changes: 12 additions & 1 deletion lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@
"no_readme": "No README available.",
"view_on_github": "View on GitHub"
},
"provenance_section": {
"title": "Provenance",
"built_and_signed_on": "Built and signed on {provider}",
"view_build_summary": "View build summary",
"source_commit": "Source Commit",
"build_file": "Build File",
"public_ledger": "Public Ledger",
"transparency_log_entry": "Transparency log entry",
"view_more_details": "View more details"
},
"keywords_title": "Keywords",
"compatibility": "Compatibility",
"card": {
Expand Down Expand Up @@ -600,7 +610,8 @@
"provenance": {
"verified": "verified",
"verified_title": "Verified provenance",
"verified_via": "Verified: published via {provider}"
"verified_via": "Verified: published via {provider}",
"view_more_details": "View more details"
},
"jsr": {
"title": "also available on JSR",
Expand Down
Loading
Loading