diff --git a/src/app/[...parts]/_page/DiffIntro/CompareSourceButton.tsx b/src/app/[...parts]/_page/DiffIntro/CompareSourceButton.tsx
new file mode 100644
index 00000000..42ed47b1
--- /dev/null
+++ b/src/app/[...parts]/_page/DiffIntro/CompareSourceButton.tsx
@@ -0,0 +1,85 @@
+import { GitCompareArrows } from "lucide-react";
+import ExternalLink from "^/components/ExternalLink";
+import Button from "^/components/ui/Button";
+import Skeleton from "^/components/ui/Skeleton";
+import getTrustEvidence from "^/lib/api/npm/getTrustEvidence";
+import { cx } from "^/lib/cva";
+import type SimplePackageSpec from "^/lib/SimplePackageSpec";
+import suspense from "^/lib/suspense";
+
+export interface CompareSourceButtonProps {
+ className?: string;
+ a: SimplePackageSpec;
+ b: SimplePackageSpec;
+}
+
+const shared = cx("my-2 flex items-center justify-center");
+
+/**
+ * Extract GitHub compare URL from two provenance URLs
+ */
+function getGitHubCompareUrl(aUrl: string, bUrl: string): string | undefined {
+ // Match both /commit/ and /tree/ URLs
+ const aMatch = aUrl.match(
+ /github\.com\/([^/]+\/[^/]+)\/(?:commit|tree)\/([a-f0-9]+)/,
+ );
+ const bMatch = bUrl.match(
+ /github\.com\/([^/]+\/[^/]+)\/(?:commit|tree)\/([a-f0-9]+)/,
+ );
+
+ if (aMatch && bMatch && aMatch[1] === bMatch[1]) {
+ const repo = aMatch[1];
+ const aCommit = aMatch[2];
+ const bCommit = bMatch[2];
+ return `https://github.com/${repo}/compare/${aCommit}...${bCommit}`;
+ }
+
+ return undefined;
+}
+
+async function CompareSourceButton({
+ a,
+ b,
+ className,
+}: CompareSourceButtonProps) {
+ const aResult = await getTrustEvidence(a);
+ const bResult = await getTrustEvidence(b);
+
+ const compareUrl =
+ aResult.provenanceUrl && bResult.provenanceUrl
+ ? getGitHubCompareUrl(aResult.provenanceUrl, bResult.provenanceUrl)
+ : undefined;
+
+ if (!compareUrl) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+function CompareSourceButtonFallback({ className }: CompareSourceButtonProps) {
+ return (
+
+
+
+ );
+}
+
+const SuspensedCompareSourceButton = suspense(
+ CompareSourceButton,
+ CompareSourceButtonFallback,
+);
+
+export default SuspensedCompareSourceButton;
diff --git a/src/app/[...parts]/_page/DiffIntro/DiffIntro.tsx b/src/app/[...parts]/_page/DiffIntro/DiffIntro.tsx
index 27a1776b..6993695a 100644
--- a/src/app/[...parts]/_page/DiffIntro/DiffIntro.tsx
+++ b/src/app/[...parts]/_page/DiffIntro/DiffIntro.tsx
@@ -35,6 +35,8 @@ const DiffIntro = forwardRef, DiffIntroProps>(
left={
}
@@ -46,6 +48,8 @@ const DiffIntro = forwardRef, DiffIntroProps>(
right={
}
diff --git a/src/app/[...parts]/_page/DiffIntro/SpecBox.tsx b/src/app/[...parts]/_page/DiffIntro/SpecBox.tsx
index 56aadf74..bbeed4e4 100644
--- a/src/app/[...parts]/_page/DiffIntro/SpecBox.tsx
+++ b/src/app/[...parts]/_page/DiffIntro/SpecBox.tsx
@@ -9,10 +9,18 @@ import ServiceLinks from "./ServiceLinks";
interface SpecBoxProps extends HTMLAttributes {
pkg: SimplePackageSpec;
pkgClassName?: string;
+ /**
+ * Other package for comparison. If provided, will show trust warnings/improvements.
+ */
+ comparisonPkg?: SimplePackageSpec;
+ /**
+ * Whether this is the "from" or "to" package in the comparison
+ */
+ isTarget?: boolean;
}
const SpecBox = forwardRef(
- ({ pkg, pkgClassName, ...props }, ref) => (
+ ({ pkg, pkgClassName, comparisonPkg, isTarget, ...props }, ref) => (
+ }
+ center={
+
+ }
+ right={
+
+ }
+ />
+ );
+}
diff --git a/src/app/[...parts]/_page/SourceDiff/TrustBadge.tsx b/src/app/[...parts]/_page/SourceDiff/TrustBadge.tsx
new file mode 100644
index 00000000..0a7a1fee
--- /dev/null
+++ b/src/app/[...parts]/_page/SourceDiff/TrustBadge.tsx
@@ -0,0 +1,163 @@
+import { Check, FileCode, Info, ShieldCheck, XCircle } from "lucide-react";
+import ExternalLink from "^/components/ExternalLink";
+import Skeleton from "^/components/ui/Skeleton";
+import Tooltip from "^/components/ui/Tooltip";
+import getTrustEvidence, {
+ type TrustEvidence,
+} from "^/lib/api/npm/getTrustEvidence";
+import { cx } from "^/lib/cva";
+import type SimplePackageSpec from "^/lib/SimplePackageSpec";
+import suspense from "^/lib/suspense";
+
+export interface TrustBadgeProps {
+ className?: string;
+ pkg: SimplePackageSpec;
+ comparisonPkg?: SimplePackageSpec;
+ isTarget?: boolean;
+}
+
+const TRUST_RANK = {
+ trustedPublisher: 2,
+ provenance: 1,
+ undefined: 0,
+} as const;
+
+function getTrustRank(evidence: TrustEvidence): number {
+ return TRUST_RANK[evidence ?? "undefined"];
+}
+
+const shared = cx(
+ "my-1 flex min-h-6 flex-col items-center justify-center gap-1",
+);
+
+function getTrustLabel(evidence: TrustEvidence): string {
+ switch (evidence) {
+ case "trustedPublisher":
+ return "Trusted Publisher";
+ case "provenance":
+ return "Provenance";
+ default:
+ return "No Evidence";
+ }
+}
+
+function getTrustTooltip(evidence: TrustEvidence): string {
+ switch (evidence) {
+ case "trustedPublisher":
+ return "Published using a trusted GitHub Actions workflow with OIDC authentication. Highest level of trust.";
+ case "provenance":
+ return "Published with SLSA provenance attestation. Cryptographically verifiable build information.";
+ default:
+ return "This package was published without provenance attestation or trusted publisher verification.";
+ }
+}
+
+function getTrustIcon(evidence: TrustEvidence): React.ReactNode {
+ const iconClass = "size-5";
+ switch (evidence) {
+ case "trustedPublisher":
+ return ;
+ case "provenance":
+ return ;
+ default:
+ return ;
+ }
+}
+
+function getTrustColor(evidence: TrustEvidence): string {
+ switch (evidence) {
+ case "trustedPublisher":
+ return "text-green-600 dark:text-green-400";
+ case "provenance":
+ return "text-blue-600 dark:text-blue-400";
+ default:
+ return "text-gray-500 dark:text-gray-400";
+ }
+}
+
+async function TrustBadge({
+ pkg,
+ className,
+ comparisonPkg,
+ isTarget,
+}: TrustBadgeProps) {
+ const { evidence, provenanceUrl } = await getTrustEvidence(pkg);
+
+ // Determine trust change if comparing
+ let isDowngrade = false;
+ let isUpgrade = false;
+ if (comparisonPkg && isTarget) {
+ const { evidence: comparisonEvidence } =
+ await getTrustEvidence(comparisonPkg);
+ const currentRank = getTrustRank(evidence);
+ const comparisonRank = getTrustRank(comparisonEvidence);
+ isDowngrade = currentRank < comparisonRank;
+ isUpgrade = currentRank > comparisonRank;
+ }
+
+ const label = getTrustLabel(evidence);
+ const tooltip = getTrustTooltip(evidence);
+ const icon = getTrustIcon(evidence);
+ const colorClass = getTrustColor(evidence);
+
+ return (
+
+
+ {icon}
+ {label}
+ {evidence !== undefined ? (
+
+
+
+ ) : null}
+
+ {isDowngrade ? (
+
+ ⚠️ Trust downgrade
+
+ ) : null}
+ {isUpgrade ? (
+
+ ✓ Trust improved
+
+ ) : null}
+ {provenanceUrl ? (
+
+
+
+ Source
+
+
+ ) : null}
+
+ );
+}
+
+function TrustBadgeFallback({ className }: TrustBadgeProps) {
+ return (
+
+
+
+ );
+}
+
+const SuspensedTrustBadge = suspense(TrustBadge, TrustBadgeFallback);
+
+export default SuspensedTrustBadge;
diff --git a/src/app/[...parts]/_page/SourceDiff/index.ts b/src/app/[...parts]/_page/SourceDiff/index.ts
new file mode 100644
index 00000000..49529f8c
--- /dev/null
+++ b/src/app/[...parts]/_page/SourceDiff/index.ts
@@ -0,0 +1,3 @@
+import SourceDiff from "./SourceDiff";
+
+export default SourceDiff;
diff --git a/src/app/[...parts]/page.tsx b/src/app/[...parts]/page.tsx
index d3d223e4..bd513f2b 100644
--- a/src/app/[...parts]/page.tsx
+++ b/src/app/[...parts]/page.tsx
@@ -15,6 +15,7 @@ import DiffIntro from "./_page/DiffIntro";
import NpmDiff from "./_page/NpmDiff";
import PackagephobiaDiff from "./_page/PackagephobiaDiff";
import { type DIFF_TYPE_PARAM_NAME } from "./_page/paramNames";
+import SourceDiff from "./_page/SourceDiff/SourceDiff";
export interface DiffPageProps {
params: Promise<{ parts: string | string[] }>;
@@ -75,6 +76,7 @@ const DiffPageInner = async ({
b={b}
services={
<>
+
;
+ };
+}
+
+interface SLSAV1Payload {
+ _type?: string;
+ subject?: Array<{
+ name?: string;
+ }>;
+ predicateType?: string;
+ predicate?: SLSAv1Predicate;
+}
+
+interface AttestationBundle {
+ predicateType?: string;
+ bundle?: {
+ dsseEnvelope?: {
+ payload: string;
+ payloadType?: string;
+ [key: string]: unknown;
+ };
+ [key: string]: unknown;
+ };
+ [key: string]: unknown;
+}
+
+interface ProvenanceReference {
+ url?: string;
+ predicateType?: string;
+}
+
+// Extended types for manifest with trust-related fields
+interface ExtendedNpmUser {
+ trustedPublisher?: boolean;
+ [key: string]: unknown;
+}
+
+interface ExtendedDist {
+ attestations?: {
+ url?: string;
+ provenance?: ProvenanceReference;
+ [key: string]: unknown;
+ };
+ [key: string]: unknown;
+}
+
+/**
+ * Get trust evidence for a specific package version from the packument.
+ * Based on pnpm's getTrustEvidence function:
+ * https://github.com/pnpm/pnpm/blob/main/resolving/npm-resolver/src/trustChecks.ts
+ */
+export function getTrustEvidenceFromManifest(
+ manifest: Manifest,
+): TrustEvidence {
+ // Check for trustedPublisher on _npmUser
+ const npmUser = manifest._npmUser as unknown as ExtendedNpmUser | undefined;
+ if (npmUser?.trustedPublisher) {
+ return "trustedPublisher";
+ }
+ // Check for provenance attestations in dist
+ const dist = manifest.dist as unknown as ExtendedDist | undefined;
+ if (dist?.attestations?.provenance) {
+ return "provenance";
+ }
+ return undefined;
+}
+
+/**
+ * Fetch attestation bundle from npm registry
+ */
+async function fetchAttestationBundle(
+ url: string,
+): Promise {
+ try {
+ const response = await fetch(url, {
+ headers: {
+ Accept: "application/json",
+ },
+ });
+ if (!response.ok) {
+ return null;
+ }
+ const data = (await response.json()) as { attestations?: unknown };
+ if (
+ data.attestations &&
+ Array.isArray(data.attestations) &&
+ data.attestations.length > 0
+ ) {
+ return data.attestations as AttestationBundle[];
+ }
+ } catch (e) {
+ console.warn("Failed to fetch attestation bundle", e);
+ }
+ return null;
+}
+
+/**
+ * Extract provenance URL from manifest if available
+ */
+async function getProvenanceUrl(
+ manifest: Manifest,
+): Promise {
+ const dist = manifest.dist as unknown as ExtendedDist | undefined;
+ const attestations = dist?.attestations;
+ if (!attestations?.url) {
+ return undefined;
+ }
+
+ // Fetch the attestation bundle from npm registry
+ const bundles = await fetchAttestationBundle(attestations.url);
+ if (!bundles) {
+ return undefined;
+ }
+
+ // Find the SLSA provenance attestation
+ const provenanceBundle = bundles.find(
+ (bundle) =>
+ bundle.bundle?.dsseEnvelope &&
+ (bundle.predicateType === "https://slsa.dev/provenance/v1" ||
+ bundle.predicateType?.includes("slsa.dev/provenance")),
+ );
+
+ if (!provenanceBundle?.bundle?.dsseEnvelope?.payload) {
+ return undefined;
+ }
+
+ try {
+ // Decode base64 payload
+ const payload = Buffer.from(
+ provenanceBundle.bundle.dsseEnvelope.payload,
+ "base64",
+ ).toString("utf-8");
+ const predicateObj = JSON.parse(payload) as SLSAV1Payload;
+
+ // Extract from SLSA v1.0 format
+ const predicate = predicateObj.predicate;
+ if (!predicate?.buildDefinition) {
+ return undefined;
+ }
+
+ // Get repository URL from external parameters
+ const repository =
+ predicate.buildDefinition.externalParameters?.workflow?.repository;
+ if (!repository) {
+ return undefined;
+ }
+
+ // Validate it's a GitHub URL (security: only allow github.com as the exact hostname)
+ if (!isValidGitHubUrl(repository)) {
+ return undefined;
+ }
+
+ // Get commit SHA from resolvedDependencies
+ const deps = predicate.buildDefinition.resolvedDependencies;
+ if (!deps || !Array.isArray(deps) || deps.length === 0) {
+ return undefined;
+ }
+
+ const commitSha = deps[0]?.digest?.gitCommit;
+ if (!commitSha || !/^[a-f0-9]{40}$/i.test(commitSha)) {
+ return undefined;
+ }
+
+ // Extract owner/repo from GitHub URL
+ const repoMatch = repository.match(
+ /^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/,
+ );
+ if (!repoMatch) {
+ return undefined;
+ }
+
+ return `https://github.com/${repoMatch[1]}/tree/${commitSha}`;
+ } catch (e) {
+ console.warn("Failed to parse provenance URL", e);
+ }
+
+ return undefined;
+}
+
+/**
+ * Validate that a URL is a proper GitHub repository URL
+ * Security: Only allow github.com as the exact hostname (not as part of path or subdomain)
+ */
+function isValidGitHubUrl(url: string): boolean {
+ try {
+ const parsed = new URL(url);
+ // Only allow github.com as the hostname
+ return parsed.hostname === "github.com" && parsed.protocol === "https:";
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Separate function that takes only packagename and version for better caching.
+ */
+async function getTrustInfoForVersion(
+ packageName: string,
+ version: string,
+): Promise {
+ "use cache";
+
+ cacheLife("hours");
+
+ const pack = await packument(packageName, {
+ // Response is too large to cache in Next's Data Cache; always fetch
+ cache: "no-store",
+ });
+
+ const manifest = pack.versions[version];
+ if (!manifest) {
+ return { evidence: undefined };
+ }
+
+ const evidence = getTrustEvidenceFromManifest(manifest);
+ // Both provenance and trustedPublisher have provenance data
+ // (trustedPublisher always includes provenance)
+ const provenanceUrl =
+ evidence === "provenance" || evidence === "trustedPublisher"
+ ? await getProvenanceUrl(manifest)
+ : undefined;
+
+ return { evidence, provenanceUrl };
+}
+
+/**
+ * Get trust information for a package spec
+ */
+export default async function getTrustEvidence(
+ spec: string | SimplePackageSpec,
+): Promise {
+ const { name, version } =
+ typeof spec === "string" ? createSimplePackageSpec(spec) : spec;
+
+ if (!name || !version) {
+ return { evidence: undefined };
+ }
+
+ return getTrustInfoForVersion(name, version);
+}
diff --git a/src/lib/api/npm/trustComparison.ts b/src/lib/api/npm/trustComparison.ts
new file mode 100644
index 00000000..30d655dc
--- /dev/null
+++ b/src/lib/api/npm/trustComparison.ts
@@ -0,0 +1,20 @@
+import getTrustEvidence, { type TrustInfo } from "./getTrustEvidence";
+
+export interface TrustComparisonResult {
+ a: TrustInfo;
+ b: TrustInfo;
+}
+
+/**
+ * Fetch trust evidence for both packages in a comparison
+ */
+export default async function trustComparison(
+ specs: [string, string],
+): Promise {
+ const [a, b] = await Promise.all([
+ getTrustEvidence(specs[0]),
+ getTrustEvidence(specs[1]),
+ ]);
+
+ return { a, b };
+}