Skip to content

Comments

feat: add provenance to end of README and provenance badge#436

Merged
danielroe merged 48 commits intonpmx-dev:mainfrom
AscaL:feature/provenance
Feb 4, 2026
Merged

feat: add provenance to end of README and provenance badge#436
danielroe merged 48 commits intonpmx-dev:mainfrom
AscaL:feature/provenance

Conversation

@AscaL
Copy link
Contributor

@AscaL AscaL commented Jan 30, 2026

Hello, i created the following:

  • Provenance API endpoint and parser for npm attestations (supports v1 and minimally v0.2). For now there is support for Github and Gitlab
  • PackageProvenanceSection and AppPopover components to display the badge in a popover and a section under the readme
  • Currently we recognize GitHub and GitLab by name, other have a generic fallback ('CI' or 'unknown' depending on builder.id) but can ofc be added.

Popover:
Clicking "view more details" will link go to the #provenance section under the readme.
image

Section at the end of the README
image

This is my first time so let me know if there is anything i need to do or change please.
Feedback is welcome :)

References #244

@vercel
Copy link

vercel bot commented Jan 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 4, 2026 11:56am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 4, 2026 11:56am
npmx-lunaria Ignored Ignored Feb 4, 2026 11:56am

Request Review

@github-actions
Copy link

github-actions bot commented Jan 30, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
lunaria/files/en-GB.json Localization changed, will be marked as complete. 🔄️
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@AscaL AscaL changed the title Add Provenance to end of reame and Provenance badge feat: add provenance to end of README and provenance badge Jan 30, 2026
@AscaL
Copy link
Contributor Author

AscaL commented Jan 30, 2026

I need also to fix a hydration error that i didn't see, and I had a few more ideas

@AscaL
Copy link
Contributor Author

AscaL commented Jan 31, 2026

Could someone more knowledgeable please check that the way i handled the hydration mismatch is correct?
Basically on the server the provenance section (and badge) was not present, while it was on the client. So i delayed rendering it until the page ([...package]) mounts.
I was also thinking, if this is the correct way, is this a case for .client components? or should i have handled this another way entirely?

And I think this bg looks more in line with the theme
image

@serhalp serhalp added this to the npm.js.com feature parity milestone Jan 31, 2026
Co-authored-by: Robin <robin.kehl@singular-it.de>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
server/api/registry/provenance/[...pkg].get.ts (2)

35-40: Consider removing redundant version check.

This version check duplicates lines 22-27. Since rawVersion is already validated as truthy before schema parsing, this second check appears unnecessary unless the schema transforms the version in a way that could make it empty.

♻️ Suggested simplification
     const parsed = v.parse(PackageRouteParamsSchema, {
       packageName: rawPackageName,
       version: rawVersion,
     })
-    const { packageName, version } = parsed
-    if (!version) {
-      throw createError({
-        statusCode: 400,
-        message: 'Version is required for provenance.',
-      })
-    }
+    const { packageName, version } = parsed as { packageName: string; version: string }

42-49: Silent null for non-existent versions may mask errors.

If version doesn't exist in the packument, versionData will be undefined, and the endpoint returns null. This is indistinguishable from a valid version without provenance. Consider returning a 404 for non-existent versions to help API consumers differentiate between "version exists but has no provenance" and "version doesn't exist".

♻️ Optional: distinguish non-existent versions
     const packument = await fetchNpmPackage(packageName)
     const versionData = packument.versions[version]
+    if (!versionData) {
+      throw createError({
+        statusCode: 404,
+        message: `Version ${version} not found for package ${packageName}.`,
+      })
+    }
     const dist = versionData?.dist as NpmVersionDist | undefined
     const attestationsUrl = dist?.attestations?.url

     if (!attestationsUrl) {
       return null
     }
app/components/PackageProvenanceSection.vue (1)

31-36: Use <i18n-t> to avoid v-html as a best practice.

The suggested change to use <i18n-t> with a named slot is technically sound and aligns with best practices to avoid v-html where possible. However, note that providerLabel is not untrusted—it is always a hardcoded safe string ('GitHub Actions', 'GitLab CI', 'CI', or 'Unknown') derived from a known provider mapping, not directly from attestation metadata.

Suggested change
-        <span
-          v-html="
-            $t('package.provenance_section.built_and_signed_on', {
-              provider: `<b>${details.providerLabel}</b>`,
-            })
-          "
-        />
+        <i18n-t keypath="package.provenance_section.built_and_signed_on" tag="span">
+          <template `#provider`>
+            <strong>{{ details.providerLabel }}</strong>
+          </template>
+        </i18n-t>

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Comment on lines 556 to 578
<template v-if="hasProvenance(displayVersion) && provenanceBadgeMounted">
<TooltipApp
:text="
provenanceData && provenanceStatus !== 'pending'
? $t('package.provenance_section.built_and_signed_on', {
provider: provenanceData.providerLabel,
})
: $t('package.verified_provenance')
"
position="bottom"
>
<a
href="#provenance"
:aria-label="$t('package.provenance_section.view_more_details')"
class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-emerald-500 transition-colors duration-200 min-w-6 min-h-6"
>
<span
class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0"
aria-hidden="true"
/>
</a>
</TooltipApp>
</template>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

head -n 600 app/pages/package/[...package].vue | tail -n 100

Repository: npmx-dev/npmx.dev

Length of output: 4689


🏁 Script executed:

wc -l app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 100


🏁 Script executed:

rg "provenanceStatus|provenanceData" app/pages/package/[...package].vue -A 2 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 868


🏁 Script executed:

rg "fetchProvenance|useLazyFetch" app/pages/package/[...package].vue -A 5 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 1703


🏁 Script executed:

rg "catch|error" app/pages/package/[...package].vue | grep -i provenance

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

web_search
Nuxt useLazyFetch status states documentation

Repository: npmx-dev/npmx.dev

Length of output: 154


🌐 Web query:

Nuxt useLazyFetch status possible values states

💡 Result:

In Nuxt, useLazyFetch() returns an AsyncData object whose status is a Ref with these possible string values:

  • "idle": request hasn’t started yet (common with lazy/non-immediate behavior, or when server: false during SSR). [1]
  • "pending": request is currently in progress. [1]
  • "success": request finished successfully. [1]
  • "error": request failed. [1]

(These are the same status values used by useFetch / useAsyncData.) [2]

Sources: [1] Nuxt useFetch docs (status values) https://nuxt.com/docs/4.x/api/composables/use-fetch, [2] Nuxt Data Fetching guide https://nuxt.com/docs/4.x/getting-started/data-fetching


🏁 Script executed:

rg "provenanceData\s*=" app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

rg "provenanceData" app/pages/package/[...package].vue | head -20

Repository: npmx-dev/npmx.dev

Length of output: 292


🏁 Script executed:

sed -n '200,250p' app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 1688


🏁 Script executed:

rg -B 20 "status: provenanceStatus" app/pages/package/[...package].vue | head -40

Repository: npmx-dev/npmx.dev

Length of output: 644


🏁 Script executed:

rg "provenanceData|fetchProvenance" app/pages/package/[...package].vue -A 10 | grep -A 10 -B 5 "error\|catch\|reset"

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

sed -n '290,330p' app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 1191


🏁 Script executed:

rg -n "const {" app/pages/package/[...package].vue | grep -i provenance

Repository: npmx-dev/npmx.dev

Length of output: 138


🏁 Script executed:

sed -n '320,360p' app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 1484


🏁 Script executed:

rg -n "useLazyFetch.*Provenance\|data: provenanceData" app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

sed -n '360,400p' app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 1306


🏁 Script executed:

grep -n "provenanceData\|provenanceStatus\|fetchProvenance" app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 461


🏁 Script executed:

sed -n '160,185p' app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 596


🏁 Script executed:

sed -n '555,570p' app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 851


🏁 Script executed:

sed -n '1130,1150p' app/pages/package/[...package].vue

Repository: npmx-dev/npmx.dev

Length of output: 766


Use explicit success status to avoid showing stale provider labels after failed fetches.

The current condition provenanceData && provenanceStatus !== 'pending' will display a stale provider label if a provenance fetch fails. When provenanceStatus becomes 'error', the data from a previous successful fetch will still exist and the tooltip will show it. Change the condition to explicitly check for success:

-                  provenanceData && provenanceStatus !== 'pending'
+                  provenanceStatus === 'success' && provenanceData
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<template v-if="hasProvenance(displayVersion) && provenanceBadgeMounted">
<TooltipApp
:text="
provenanceData && provenanceStatus !== 'pending'
? $t('package.provenance_section.built_and_signed_on', {
provider: provenanceData.providerLabel,
})
: $t('package.verified_provenance')
"
position="bottom"
>
<a
href="#provenance"
:aria-label="$t('package.provenance_section.view_more_details')"
class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-emerald-500 transition-colors duration-200 min-w-6 min-h-6"
>
<span
class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0"
aria-hidden="true"
/>
</a>
</TooltipApp>
</template>
<template v-if="hasProvenance(displayVersion) && provenanceBadgeMounted">
<TooltipApp
:text="
provenanceStatus === 'success' && provenanceData
? $t('package.provenance_section.built_and_signed_on', {
provider: provenanceData.providerLabel,
})
: $t('package.verified_provenance')
"
position="bottom"
>
<a
href="#provenance"
:aria-label="$t('package.provenance_section.view_more_details')"
class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-emerald-500 transition-colors duration-200 min-w-6 min-h-6"
>
<span
class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0"
aria-hidden="true"
/>
</a>
</TooltipApp>
</template>

Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

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

❤️

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
server/utils/provenance.ts (1)

117-131: Prefer selecting attestation entries by content instead of fixed index.

Lines 117-131 assume the first resolved dependency/tlog entry is the one you want. If ordering changes, you may silently lose the commit or log index. A find() on the relevant fields is more robust.

♻️ Suggested refactor
-  const resolved = pred.buildDefinition?.resolvedDependencies?.[0]
-  const commitSha = resolved?.digest?.gitCommit ?? ''
+  const resolved = pred.buildDefinition?.resolvedDependencies?.find(dep => dep.digest?.gitCommit)
+  const commitSha = resolved?.digest?.gitCommit ?? ''

   const tlogEntries = slsaAttestation.bundle.verificationMaterial?.tlogEntries
-  const logIndex = tlogEntries?.[0]?.logIndex
+  const logIndex = tlogEntries?.find(entry => entry.logIndex)?.logIndex

As per coding guidelines, ensure you always check when accessing an array value by index.

app/pages/package/[...package].vue (1)

162-187: Simplify refetch logic—useLazyFetch with a getter URL auto-refetches on changes.

Nuxt 4's useLazyFetch() automatically refetches when the URL expression is reactive (via a getter function or computed). Here, the URL is already a getter that depends on displayVersion, so Nuxt will refetch whenever displayVersion changes—even with immediate: false.

The manual watch on displayVersion with the provenanceStatus === 'idle' guard appears redundant and may cause duplicate fetch attempts. Either:

  • Remove the manual watch and rely on Nuxt's auto-refetch, or
  • If explicit control is needed, document why and consider disabling auto-watch with watch: false in the fetch options.

Merged via the queue into npmx-dev:main with commit bf0c8f7 Feb 4, 2026
16 checks passed
@AscaL AscaL deleted the feature/provenance branch February 4, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants