Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 85 additions & 0 deletions src/app/[...parts]/_page/DiffIntro/CompareSourceButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={cx(shared, className)}>
<Button variant="secondary" size="sm" asChild>
<ExternalLink
href={compareUrl}
className="inline-flex items-center gap-2"
>
<GitCompareArrows className="size-4" />
Compare source
</ExternalLink>
</Button>
</div>
);
}

function CompareSourceButtonFallback({ className }: CompareSourceButtonProps) {
return (
<div className={cx(shared, className)}>
<Skeleton className="h-8 w-32" />
</div>
);
}

const SuspensedCompareSourceButton = suspense(
CompareSourceButton,
CompareSourceButtonFallback,
);

export default SuspensedCompareSourceButton;
4 changes: 4 additions & 0 deletions src/app/[...parts]/_page/DiffIntro/DiffIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const DiffIntro = forwardRef<ElementRef<typeof Stack>, DiffIntroProps>(
left={
<SpecBox
pkg={aWithName}
comparisonPkg={bWithName}
isTarget={false}
pkgClassName="rounded-r-none"
/>
}
Expand All @@ -46,6 +48,8 @@ const DiffIntro = forwardRef<ElementRef<typeof Stack>, DiffIntroProps>(
right={
<SpecBox
pkg={bWithName}
comparisonPkg={aWithName}
isTarget={true}
pkgClassName="rounded-l-none"
/>
}
Expand Down
10 changes: 9 additions & 1 deletion src/app/[...parts]/_page/DiffIntro/SpecBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import ServiceLinks from "./ServiceLinks";
interface SpecBoxProps extends HTMLAttributes<HTMLElement> {
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<HTMLElement, SpecBoxProps>(
({ pkg, pkgClassName, ...props }, ref) => (
({ pkg, pkgClassName, comparisonPkg, isTarget, ...props }, ref) => (
<section {...props} ref={ref}>
<Pkg pkg={pkg} className={cx("px-1", pkgClassName)} />
<PublishDate
Expand Down
42 changes: 42 additions & 0 deletions src/app/[...parts]/_page/SourceDiff/SourceDiff.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type SimplePackageSpec from "^/lib/SimplePackageSpec";
import { simplePackageSpecToString } from "^/lib/SimplePackageSpec";
import SuspensedCompareSourceButton from "../DiffIntro/CompareSourceButton";
import Halfs from "../DiffIntro/Halfs";
import TrustBadge from "./TrustBadge";

export interface SourceDiffProps {
a: SimplePackageSpec;
b: SimplePackageSpec;
}

export default function SourceDiff({ a, b }: SourceDiffProps) {
return (
<Halfs
left={
<TrustBadge
suspenseKey={"trustbadge-" + simplePackageSpecToString(a)}
pkg={a}
comparisonPkg={b}
isTarget={false}
className="font-normal"
/>
}
center={
<SuspensedCompareSourceButton
suspenseKey={"comparesource-" + a.name + b.name}
a={a}
b={b}
/>
}
right={
<TrustBadge
suspenseKey={"trustbadge-" + simplePackageSpecToString(b)}
pkg={b}
comparisonPkg={a}
isTarget={true}
className="font-normal"
/>
}
/>
);
}
163 changes: 163 additions & 0 deletions src/app/[...parts]/_page/SourceDiff/TrustBadge.tsx
Original file line number Diff line number Diff line change
@@ -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 <ShieldCheck className={iconClass} />;
case "provenance":
return <Check className={iconClass} />;
default:
return <XCircle className={iconClass} />;
}
}

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 (
<div className={cx(shared, className)}>
<div
className={cx(
"flex items-center justify-center gap-1 rounded px-2 py-1",
colorClass,
"text-sm font-semibold",
isDowngrade &&
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",
)}
>
{icon}
<span>{label}</span>
{evidence !== undefined ? (
<Tooltip label={tooltip}>
<button
type="button"
className="inline-flex cursor-help opacity-60 hover:opacity-100"
aria-label="More information"
>
<Info className="size-3.5" />
</button>
</Tooltip>
) : null}
</div>
{isDowngrade ? (
<div className="text-xs font-semibold text-red-600 dark:text-red-400">
⚠️ Trust downgrade
</div>
) : null}
{isUpgrade ? (
<div className="text-xs font-semibold text-green-600 dark:text-green-400">
✓ Trust improved
</div>
) : null}
{provenanceUrl ? (
<div className="text-xs">
<ExternalLink
href={provenanceUrl}
className="inline-flex items-center gap-1 text-blue-600 hover:underline dark:text-blue-400"
>
<FileCode className="size-3" />
Source
</ExternalLink>
</div>
) : null}
</div>
);
}

function TrustBadgeFallback({ className }: TrustBadgeProps) {
return (
<div className={cx(shared, className)}>
<Skeleton className="h-3 w-24" />
</div>
);
}

const SuspensedTrustBadge = suspense(TrustBadge, TrustBadgeFallback);

export default SuspensedTrustBadge;
3 changes: 3 additions & 0 deletions src/app/[...parts]/_page/SourceDiff/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import SourceDiff from "./SourceDiff";

export default SourceDiff;
2 changes: 2 additions & 0 deletions src/app/[...parts]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] }>;
Expand Down Expand Up @@ -75,6 +76,7 @@ const DiffPageInner = async ({
b={b}
services={
<>
<SourceDiff a={a} b={b} />
<BundlephobiaDiff
a={a}
b={b}
Expand Down
Loading