Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -44,7 +44,7 @@ What npmx offers:
- **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
96 changes: 96 additions & 0 deletions app/components/PackageProvenanceSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script setup lang="ts">
import type { ProvenanceDetails } from '#shared/types'

defineProps<{
details: ProvenanceDetails
}>()
</script>

<template>
<section 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
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-lucide-shield-check w-4 h-4 shrink-0 text-emerald-500" aria-hidden="true" />
<i18n-t keypath="package.provenance_section.built_and_signed_on" tag="span">
<template #provider>
<strong>{{ details.providerLabel }}</strong>
</template>
</i18n-t>
</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>
</section>
</template>
95 changes: 84 additions & 11 deletions app/pages/package/[...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 @@ -158,6 +159,39 @@ const { data: vulnTree, status: vulnTreeStatus } = useDependencyAnalysis(
() => resolvedVersion.value ?? '',
)

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) {
watch(
displayVersion,
v => {
if (v && hasProvenance(v) && provenanceStatus.value === 'idle') {
fetchProvenance()
}
},
{ immediate: true },
)
}

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 @@ -523,16 +557,26 @@ defineOgImageComponent('Package', {
>
<span v-else>v{{ resolvedVersion }}</span>

<a
v-if="hasProvenance(displayVersion)"
:href="`https://www.npmjs.com/package/${pkg.name}/v/${resolvedVersion}#provenance`"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-fg transition-colors duration-200 min-w-6 min-h-6"
:title="$t('package.verified_provenance')"
>
<span class="i-lucide-shield-check w-3.5 h-3.5 shrink-0" aria-hidden="true" />
</a>
<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-lucide-shield-check w-3.5 h-3.5 shrink-0" aria-hidden="true" />
</a>
</TooltipApp>
</template>
Comment on lines 560 to 579
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>

<span
v-if="requestedVersion && latestVersion && resolvedVersion !== latestVersion.version"
class="text-fg-subtle text-sm shrink-0"
Expand Down Expand Up @@ -1084,8 +1128,37 @@ defineOgImageComponent('Package', {
>{{ $t('package.readme.view_on_github') }}</a
>
</p>
</section>

<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"
class="mt-8"
/>
<!-- Error state: provenance exists but details failed to load -->
<div
v-else-if="provenanceStatus === 'error'"
class="mt-8 flex items-center gap-2 text-fg-subtle text-sm"
>
<span class="i-carbon:warning w-4 h-4" aria-hidden="true" />
<span>{{ $t('package.provenance_section.error_loading') }}</span>
</div>
</section>
</section>
<div class="area-sidebar">
<!-- Sidebar -->
<div class="sticky top-34 space-y-6 sm:space-y-8 min-w-0 overflow-hidden xl:(top-22) pt-1">
Expand Down
14 changes: 13 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@
"view_on_github": "View on GitHub",
"toc_title": "Outline"
},
"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",
"error_loading": "Failed to load provenance details"
},
"keywords_title": "Keywords",
"compatibility": "Compatibility",
"card": {
Expand Down Expand Up @@ -610,7 +621,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
14 changes: 13 additions & 1 deletion lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@
"view_on_github": "View on GitHub",
"toc_title": "Outline"
},
"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",
"error_loading": "Failed to load provenance details"
},
"keywords_title": "Keywords",
"compatibility": "Compatibility",
"card": {
Expand Down Expand Up @@ -610,7 +621,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
14 changes: 13 additions & 1 deletion lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@
"view_on_github": "View on GitHub",
"toc_title": "Outline"
},
"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",
"error_loading": "Failed to load provenance details"
},
"keywords_title": "Keywords",
"compatibility": "Compatibility",
"card": {
Expand Down Expand Up @@ -610,7 +621,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
1 change: 1 addition & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export default defineNuxtConfig({
'/package-docs/:scope/:pkg/v/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
'/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
'/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
'/api/registry/provenance/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
'/api/registry/files/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
'/_avatar/**': {
isr: 3600,
Expand Down
69 changes: 69 additions & 0 deletions server/api/registry/provenance/[...pkg].get.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the question, but is this file AI generated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No issue, yes it is for the most part, beside some minor changes I made

Copy link
Contributor

Choose a reason for hiding this comment

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

No issue, yes it is for the most part, beside some minor changes I made

Oh..

I'm rewriting this code currently

Copy link
Contributor

Choose a reason for hiding this comment

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

This PR still needs changes and reviews, so it's better not to merge it for now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, what's the issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still working on this

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, what's the issue?

It is described here - #436 (review)

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as v from 'valibot'
import { PackageRouteParamsSchema } from '#shared/schemas/package'
import type { NpmVersionDist } from '#shared/types'
import { CACHE_MAX_AGE_ONE_HOUR, ERROR_PROVENANCE_FETCH_FAILED } from '#shared/utils/constants'
import {
parseAttestationToProvenanceDetails,
type NpmAttestationsResponse,
} from '#server/utils/provenance'

/**
* GET /api/registry/provenance/:name/v/:version
*
* Returns parsed provenance details for a package version (build summary, source commit, build file, public ledger).
* Version is required. Returns null when the version has no attestations or parsing fails.
*/
export default defineCachedEventHandler(
async event => {
const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []

const { rawPackageName, rawVersion } = parsePackageParams(pkgParamSegments)

if (!rawVersion) {
throw createError({
statusCode: 400,
message: 'Version is required for provenance.',
})
}

try {
const parsed = v.parse(PackageRouteParamsSchema, {
packageName: rawPackageName,
version: rawVersion,
}) as { packageName: string; version: string }
const { packageName, version } = parsed

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
}

const response = await $fetch<NpmAttestationsResponse>(attestationsUrl)
const details = parseAttestationToProvenanceDetails(response)
return details
} catch (error: unknown) {
handleApiError(error, {
statusCode: 502,
message: ERROR_PROVENANCE_FETCH_FAILED,
})
}
},
{
maxAge: CACHE_MAX_AGE_ONE_HOUR,
swr: true,
getKey: event => {
const pkg = getRouterParam(event, 'pkg') ?? ''
return `provenance:v1:${pkg.replace(/\/+$/, '').trim()}`
},
},
)
Loading
Loading