Skip to content

Commit 14356e2

Browse files
CopilotoBusk
andcommitted
Implement trust level comparison feature
- Created API functions to fetch trust evidence from packument - Created TrustComparison component to display trust levels - Created TrustDiff component with suspense wrapper - Added TrustDiff above other service comparisons in page.tsx - Supports provenance and trustedPublisher trust evidence - Shows trust downgrade/upgrade warnings with colors - Includes provenance link functionality Co-authored-by: oBusk <13413409+oBusk@users.noreply.github.com>
1 parent d3e464b commit 14356e2

File tree

7 files changed

+399
-1
lines changed

7 files changed

+399
-1
lines changed

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/dev/types/routes.d.ts";
3+
import "./.next/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { type FunctionComponent } from "react";
2+
import ExternalLink from "^/components/ExternalLink";
3+
import type { TrustEvidence } from "^/lib/api/npm/getTrustEvidence";
4+
import { cx } from "^/lib/cva";
5+
import type SimplePackageSpec from "^/lib/SimplePackageSpec";
6+
import Halfs from "../DiffIntro/Halfs";
7+
8+
const TRUST_RANK = {
9+
trustedPublisher: 2,
10+
provenance: 1,
11+
undefined: 0,
12+
} as const;
13+
14+
function getTrustRank(evidence: TrustEvidence): number {
15+
return TRUST_RANK[evidence ?? "undefined"];
16+
}
17+
18+
function getTrustLabel(evidence: TrustEvidence): string {
19+
switch (evidence) {
20+
case "trustedPublisher":
21+
return "Trusted Publisher";
22+
case "provenance":
23+
return "Provenance";
24+
default:
25+
return "No trust evidence";
26+
}
27+
}
28+
29+
function getTrustColor(evidence: TrustEvidence, isDowngrade: boolean): string {
30+
if (isDowngrade) {
31+
return "text-red-600 dark:text-red-400";
32+
}
33+
switch (evidence) {
34+
case "trustedPublisher":
35+
return "text-green-600 dark:text-green-400";
36+
case "provenance":
37+
return "text-blue-600 dark:text-blue-400";
38+
default:
39+
return "text-gray-500 dark:text-gray-400";
40+
}
41+
}
42+
43+
interface TrustBoxProps {
44+
evidence: TrustEvidence;
45+
provenanceUrl?: string;
46+
isDowngrade?: boolean;
47+
isUpgrade?: boolean;
48+
}
49+
50+
const TrustBox: FunctionComponent<TrustBoxProps> = ({
51+
evidence,
52+
provenanceUrl,
53+
isDowngrade = false,
54+
isUpgrade = false,
55+
}) => {
56+
const label = getTrustLabel(evidence);
57+
const colorClass = getTrustColor(evidence, isDowngrade);
58+
59+
const content = (
60+
<div className={cx("p-2 text-center", colorClass)}>
61+
<p className="font-semibold">{label}</p>
62+
{isDowngrade ? <p className="text-xs">⚠️ Trust downgrade</p> : null}
63+
{isUpgrade ? <p className="text-xs">✓ Trust improvement</p> : null}
64+
</div>
65+
);
66+
67+
if (provenanceUrl) {
68+
return (
69+
<ExternalLink
70+
href={provenanceUrl}
71+
className="rounded-lg hover:bg-muted"
72+
>
73+
{content}
74+
</ExternalLink>
75+
);
76+
}
77+
78+
return content;
79+
};
80+
81+
export interface TrustComparisonProps {
82+
a: SimplePackageSpec;
83+
b: SimplePackageSpec;
84+
aEvidence: TrustEvidence;
85+
bEvidence: TrustEvidence;
86+
aProvenanceUrl?: string;
87+
bProvenanceUrl?: string;
88+
}
89+
90+
/** The padding of the center column and the right/left half has to be the same to line up */
91+
const COMMON_PADDING = "p-2";
92+
93+
const TrustComparison: FunctionComponent<TrustComparisonProps> = ({
94+
aEvidence,
95+
bEvidence,
96+
aProvenanceUrl,
97+
bProvenanceUrl,
98+
}) => {
99+
const aRank = getTrustRank(aEvidence);
100+
const bRank = getTrustRank(bEvidence);
101+
const isDowngrade = bRank < aRank;
102+
const isUpgrade = bRank > aRank;
103+
104+
const compareUrl =
105+
aProvenanceUrl && bProvenanceUrl
106+
? getGitHubCompareUrl(aProvenanceUrl, bProvenanceUrl)
107+
: undefined;
108+
109+
return (
110+
<>
111+
<div className={cx("mb-2 text-center text-sm font-semibold")}>
112+
Trust Level
113+
</div>
114+
<Halfs
115+
className="w-full"
116+
left={
117+
<TrustBox
118+
evidence={aEvidence}
119+
provenanceUrl={aProvenanceUrl}
120+
/>
121+
}
122+
center={
123+
<section className={cx(COMMON_PADDING, "text-center")}>
124+
<p>Trust</p>
125+
</section>
126+
}
127+
right={
128+
<TrustBox
129+
evidence={bEvidence}
130+
provenanceUrl={bProvenanceUrl}
131+
isDowngrade={isDowngrade}
132+
isUpgrade={isUpgrade}
133+
/>
134+
}
135+
/>
136+
{compareUrl ? (
137+
<div className="mt-2 text-center text-xs">
138+
<ExternalLink
139+
href={compareUrl}
140+
className="text-blue-600 hover:underline dark:text-blue-400"
141+
>
142+
Compare source on GitHub
143+
</ExternalLink>
144+
</div>
145+
) : null}
146+
</>
147+
);
148+
};
149+
150+
/**
151+
* Extract GitHub compare URL from two provenance URLs
152+
*/
153+
function getGitHubCompareUrl(aUrl: string, bUrl: string): string | undefined {
154+
const aMatch = aUrl.match(
155+
/github\.com\/([^/]+\/[^/]+)\/commit\/([a-f0-9]+)/,
156+
);
157+
const bMatch = bUrl.match(
158+
/github\.com\/([^/]+\/[^/]+)\/commit\/([a-f0-9]+)/,
159+
);
160+
161+
if (aMatch && bMatch && aMatch[1] === bMatch[1]) {
162+
const repo = aMatch[1];
163+
const aCommit = aMatch[2];
164+
const bCommit = bMatch[2];
165+
return `https://github.com/${repo}/compare/${aCommit}...${bCommit}`;
166+
}
167+
168+
return undefined;
169+
}
170+
171+
export default TrustComparison;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { type FunctionComponent } from "react";
2+
import Skeleton from "^/components/ui/Skeleton";
3+
import { cx } from "^/lib/cva";
4+
import Halfs from "../DiffIntro/Halfs";
5+
6+
const COMMON_PADDING = "p-2";
7+
8+
export const TrustComparisonSkeleton: FunctionComponent = () => {
9+
return (
10+
<>
11+
<div className={cx("mb-2 text-center text-sm font-semibold")}>
12+
Trust Level
13+
</div>
14+
<Halfs
15+
className="w-full"
16+
left={
17+
<div className={cx(COMMON_PADDING, "text-center")}>
18+
<Skeleton className="mx-auto h-6 w-32" />
19+
</div>
20+
}
21+
center={
22+
<section className={cx(COMMON_PADDING, "text-center")}>
23+
<p>Trust</p>
24+
</section>
25+
}
26+
right={
27+
<div className={cx(COMMON_PADDING, "text-center")}>
28+
<Skeleton className="mx-auto h-6 w-32" />
29+
</div>
30+
}
31+
/>
32+
</>
33+
);
34+
};
35+
36+
export { default } from "./TrustComparison";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { cacheLife } from "next/cache";
2+
import trustComparison from "^/lib/api/npm/trustComparison";
3+
import type SimplePackageSpec from "^/lib/SimplePackageSpec";
4+
import suspense from "^/lib/suspense";
5+
import measuredPromise from "^/lib/utils/measuredPromise";
6+
import TrustComparison, { TrustComparisonSkeleton } from "./TrustComparison";
7+
8+
export interface TrustDiffProps {
9+
a: SimplePackageSpec;
10+
b: SimplePackageSpec;
11+
specs: [string, string];
12+
}
13+
14+
const TrustDiffInner = async ({ specs, a, b }: TrustDiffProps) => {
15+
"use cache";
16+
17+
// Cache for hours since trust evidence doesn't change often
18+
cacheLife("hours");
19+
20+
const { result, time } = await measuredPromise(trustComparison(specs));
21+
22+
console.log("Trust comparison", { specs, time });
23+
24+
// Don't render if both packages have no trust evidence
25+
if (result.a.evidence == null && result.b.evidence == null) {
26+
return null;
27+
}
28+
29+
return (
30+
<TrustComparison
31+
a={a}
32+
b={b}
33+
aEvidence={result.a.evidence}
34+
bEvidence={result.b.evidence}
35+
aProvenanceUrl={result.a.provenanceUrl}
36+
bProvenanceUrl={result.b.provenanceUrl}
37+
/>
38+
);
39+
};
40+
41+
const TrustDiffSkeleton = () => <TrustComparisonSkeleton />;
42+
43+
const TrustDiff = suspense(TrustDiffInner, TrustDiffSkeleton);
44+
45+
export default TrustDiff;

src/app/[...parts]/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import DiffIntro from "./_page/DiffIntro";
1515
import NpmDiff from "./_page/NpmDiff";
1616
import PackagephobiaDiff from "./_page/PackagephobiaDiff";
1717
import { type DIFF_TYPE_PARAM_NAME } from "./_page/paramNames";
18+
import TrustDiff from "./_page/TrustDiff";
1819

1920
export interface DiffPageProps {
2021
params: Promise<{ parts: string | string[] }>;
@@ -75,6 +76,14 @@ const DiffPageInner = async ({
7576
b={b}
7677
services={
7778
<>
79+
<TrustDiff
80+
a={a}
81+
b={b}
82+
specs={canonicalSpecs}
83+
suspenseKey={
84+
"trust-" + canonicalSpecs.join("...")
85+
}
86+
/>
7887
<BundlephobiaDiff
7988
a={a}
8089
b={b}

0 commit comments

Comments
 (0)