0import { loadPublicReceiptsPage, type PublicReceiptRow } from "@/db/queries";
1import {
2 loadCrossRepoReceipts,
3 type CrossRepoReceiptRow,
4} from "@/lib/outgoing-prs";
5import { renderRssFeed, type RssItem } from "@/lib/rss";
6
7// RSS 2.0 feed for the public receipt corpus. Consumed by monitoring tools
8// (Pingdom, BetterStack), RSS readers, and Slack's /feed integration. The
9// feed is the same gate as the /receipts page — only reviews with
10// public_receipt = true reach this surface, and aggregates count blind
11// across all installs but the per-item stream is opt-in only.
12// Cross-repo receipts (outgoing PRs that upstream owners merged) are
13// interleaved by pubDate with a <category>cross-repo</category> tag so
14// subscribers can filter — same trust surface but different artifact
15// shape, and the consumer should know which they're looking at.
16export const runtime = "nodejs";
17export const dynamic = "force-dynamic";
18
19const FEED_LIMIT = 50;
20const CROSS_REPO_LIMIT = 20;
21const SITE_URL = "https://www.antfleet.dev";
22
23export async function GET(): Promise<Response> {
24 const [{ recent, lastUpdatedAt }, crossRepo] = await Promise.all([
25 loadPublicReceiptsPage({ limit: FEED_LIMIT }),
26 loadCrossRepoReceipts(CROSS_REPO_LIMIT),
27 ]);
28
29 const sameRepoItems: RssItem[] = recent
30 .filter((r): r is PublicReceiptRow & { closedAt: Date } => r.closedAt !== null)
31 .map((r) => ({
32 title: `${r.category} · ${r.severity} — ${r.title}`,
33 link: `${SITE_URL}/receipts/${encodeURIComponent(r.findingId)}`,
34 guid: r.findingId,
35 pubDate: r.closedAt,
36 description: buildDescription(r),
37 }));
38
39 const crossRepoItems: RssItem[] = crossRepo.recent.map((r) => ({
40 title: `cross-repo · AntFleet → ${r.upstreamOwner}/${r.upstreamRepo} PR #${r.upstreamPrNumber}`,
41 link: r.prUrl,
42 // guid prefixed with `cross-repo:` so it can never collide with a
43 // same-repo finding_id even if both happen to share the same uuid prefix.
44 guid: `cross-repo:${r.id}`,
45 pubDate: r.mergedAt,
46 description: buildCrossRepoDescription(r),
47 category: "cross-repo",
48 }));
49
50 const items = [...sameRepoItems, ...crossRepoItems].toSorted(
51 (a, b) => b.pubDate.getTime() - a.pubDate.getTime(),
52 );
53 const newestCrossRepo = crossRepo.lastMergedAt;
54 const newestSameRepo = lastUpdatedAt;
55 const lastBuildDate =
56 newestSameRepo === null
57 ? (newestCrossRepo ?? new Date())
58 : newestCrossRepo === null
59 ? newestSameRepo
60 : newestSameRepo > newestCrossRepo
61 ? newestSameRepo
62 : newestCrossRepo;
63
64 const xml = renderRssFeed({
65 title: "AntFleet · Receipts",
66 link: `${SITE_URL}/receipts`,
67 description:
68 "Public, SHA-pinned closure receipts from the AntFleet agreement gate. Every entry is third-party-witnessed on GitHub.",
69 selfLink: `${SITE_URL}/receipts.rss`,
70 lastBuildDate,
71 items,
72 });
73
74 return new Response(xml, {
75 status: 200,
76 headers: {
77 "Content-Type": "application/rss+xml; charset=utf-8",
78 "Cache-Control": "public, max-age=300, s-maxage=300",
79 },
80 });
81}
82
83function buildDescription(row: PublicReceiptRow): string {
84 const parts: string[] = [];
85 parts.push(`repo ${row.repoHash.slice(0, 8)}`);
86 parts.push(`PR #${row.prNumber}`);
87 if (row.closureSha !== null) {
88 parts.push(`closed in ${row.closureSha.slice(0, 7)}`);
89 }
90 return parts.join(" · ");
91}
92
93function buildCrossRepoDescription(row: CrossRepoReceiptRow): string {
94 return [
95 `AntFleet → ${row.upstreamOwner}/${row.upstreamRepo}`,
96 `PR #${row.upstreamPrNumber}`,
97 `merged at ${row.mergeSha.slice(0, 7)}`,
98 ].join(" · ");
99}
100