From 5677a02f0473aa2759a4ea9e27a5ddd825cbc80b Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Mon, 25 May 2026 23:03:11 -0500 Subject: [PATCH 1/2] fix: enforce tracked-repo allowlist on gt miners and prs routes Completes allowlist coverage for /api/gt/repos/[owner]/[name]/* so untracked repos cannot trigger SQLite reads or link backfills. Fixes #141 Co-authored-by: Cursor --- src/app/api/gt/repos/[owner]/[name]/miners/route.ts | 3 +++ src/app/api/gt/repos/[owner]/[name]/prs/route.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/app/api/gt/repos/[owner]/[name]/miners/route.ts b/src/app/api/gt/repos/[owner]/[name]/miners/route.ts index 5ac9010..1ee8123 100644 --- a/src/app/api/gt/repos/[owner]/[name]/miners/route.ts +++ b/src/app/api/gt/repos/[owner]/[name]/miners/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb } from '@/lib/db'; import { backfillPrIssueLinksIfNeeded } from '@/lib/refresh'; @@ -102,6 +103,8 @@ async function getShared(): Promise { export async function GET(_req: Request, ctx: { params: Promise<{ owner: string; name: string }> }) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const fullName = `${params.owner}/${params.name}`; const fullNameKey = fullName.toLowerCase(); try { diff --git a/src/app/api/gt/repos/[owner]/[name]/prs/route.ts b/src/app/api/gt/repos/[owner]/[name]/prs/route.ts index 6d5cd91..de6fd8e 100644 --- a/src/app/api/gt/repos/[owner]/[name]/prs/route.ts +++ b/src/app/api/gt/repos/[owner]/[name]/prs/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import type { GtRepoPr, GtRepoPrsResponse } from '@/types/entities'; export const dynamic = 'force-dynamic'; @@ -91,6 +92,8 @@ async function getShared(): Promise { export async function GET(_req: Request, ctx: { params: Promise<{ owner: string; name: string }> }) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const fullName = `${params.owner}/${params.name}`; try { const shared = await getShared(); From 32f6d84dc2d4940f96c5366dd58f625c730a33a7 Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Mon, 25 May 2026 23:12:42 -0500 Subject: [PATCH 2/2] fix: enforce tracked-repo allowlist on remaining repos API routes Add assertTrackedRepo to list/meta, badges, validations, author feeds, and related-prs index handlers so untracked owner/name pairs cannot read hub cache data. Co-authored-by: Cursor --- src/app/api/related-prs/[owner]/[name]/route.ts | 3 +++ .../api/repos/[owner]/[name]/authors/[login]/issues/route.ts | 3 +++ .../api/repos/[owner]/[name]/authors/[login]/pulls/route.ts | 3 +++ src/app/api/repos/[owner]/[name]/badges/route.ts | 3 +++ src/app/api/repos/[owner]/[name]/issues-meta/route.ts | 3 +++ src/app/api/repos/[owner]/[name]/issues/route.ts | 3 +++ src/app/api/repos/[owner]/[name]/pulls-meta/route.ts | 3 +++ src/app/api/repos/[owner]/[name]/pulls/route.ts | 3 +++ src/app/api/repos/[owner]/[name]/validations/[number]/route.ts | 3 +++ 9 files changed, 27 insertions(+) diff --git a/src/app/api/related-prs/[owner]/[name]/route.ts b/src/app/api/related-prs/[owner]/[name]/route.ts index 30dec44..a5a97de 100644 --- a/src/app/api/related-prs/[owner]/[name]/route.ts +++ b/src/app/api/related-prs/[owner]/[name]/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb } from '@/lib/db'; import { backfillPrIssueLinksIfNeeded } from '@/lib/refresh'; import { buildEtag, etagNotModified, withEtagHeaders } from '@/lib/etag'; @@ -16,6 +17,8 @@ export async function GET( ctx: { params: Promise<{ owner: string; name: string }> } ) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const repo = `${params.owner}/${params.name}`; const db = getReadDb(); diff --git a/src/app/api/repos/[owner]/[name]/authors/[login]/issues/route.ts b/src/app/api/repos/[owner]/[name]/authors/[login]/issues/route.ts index 03f6e7f..23e1e34 100644 --- a/src/app/api/repos/[owner]/[name]/authors/[login]/issues/route.ts +++ b/src/app/api/repos/[owner]/[name]/authors/[login]/issues/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb, type IssueRow } from '@/lib/db'; import { authorCredibilityForRepo, getGittensorCredibilityIndex } from '@/lib/gittensor-credibility'; import { getIssueDiscoveryDisabledReposAsyncServer } from '@/lib/repos-server'; @@ -24,6 +25,8 @@ export async function GET( ctx: { params: Promise<{ owner: string; name: string; login: string }> }, ) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const full = `${params.owner}/${params.name}`; const login = params.login; const url = new URL(req.url); diff --git a/src/app/api/repos/[owner]/[name]/authors/[login]/pulls/route.ts b/src/app/api/repos/[owner]/[name]/authors/[login]/pulls/route.ts index 2e6a9b1..952de08 100644 --- a/src/app/api/repos/[owner]/[name]/authors/[login]/pulls/route.ts +++ b/src/app/api/repos/[owner]/[name]/authors/[login]/pulls/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb, type PullRow } from '@/lib/db'; import { authorCredibilityForRepo, getGittensorCredibilityIndex } from '@/lib/gittensor-credibility'; import { getIssueDiscoveryDisabledReposAsyncServer } from '@/lib/repos-server'; @@ -14,6 +15,8 @@ export async function GET( ctx: { params: Promise<{ owner: string; name: string; login: string }> }, ) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const full = `${params.owner}/${params.name}`; const login = params.login; const url = new URL(req.url); diff --git a/src/app/api/repos/[owner]/[name]/badges/route.ts b/src/app/api/repos/[owner]/[name]/badges/route.ts index 6ca6d12..9e2f8d6 100644 --- a/src/app/api/repos/[owner]/[name]/badges/route.ts +++ b/src/app/api/repos/[owner]/[name]/badges/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getDb } from '@/lib/db'; import { buildEtag, etagNotModified, withEtagHeaders } from '@/lib/etag'; @@ -26,6 +27,8 @@ export async function GET( ctx: { params: Promise<{ owner: string; name: string }> }, ) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const full = `${params.owner}/${params.name}`; const db = getDb(); diff --git a/src/app/api/repos/[owner]/[name]/issues-meta/route.ts b/src/app/api/repos/[owner]/[name]/issues-meta/route.ts index 7b5b958..f352a62 100644 --- a/src/app/api/repos/[owner]/[name]/issues-meta/route.ts +++ b/src/app/api/repos/[owner]/[name]/issues-meta/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb } from '@/lib/db'; import { backfillPrIssueLinksIfNeeded } from '@/lib/refresh'; import { buildEtag, etagNotModified, withEtagHeaders } from '@/lib/etag'; @@ -13,6 +14,8 @@ export async function GET( ctx: { params: Promise<{ owner: string; name: string }> } ) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const full = `${params.owner}/${params.name}`; const db = getReadDb(); const url = new URL(req.url); diff --git a/src/app/api/repos/[owner]/[name]/issues/route.ts b/src/app/api/repos/[owner]/[name]/issues/route.ts index 799e791..8520961 100644 --- a/src/app/api/repos/[owner]/[name]/issues/route.ts +++ b/src/app/api/repos/[owner]/[name]/issues/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb, IssueRow } from '@/lib/db'; import { refreshIssuesIfStale, backfillPrIssueLinksIfNeeded } from '@/lib/refresh'; import { buildEtag, etagNotModified, withEtagHeaders } from '@/lib/etag'; @@ -37,6 +38,8 @@ export async function GET( ) { const params = await ctx.params; const { owner, name } = params; + const denied = await assertTrackedRepo(owner, name); + if (denied) return denied; const full = `${owner}/${name}`; // Refresh is the poller's job — calling it from per-request handlers caused diff --git a/src/app/api/repos/[owner]/[name]/pulls-meta/route.ts b/src/app/api/repos/[owner]/[name]/pulls-meta/route.ts index e3e529f..7efcf83 100644 --- a/src/app/api/repos/[owner]/[name]/pulls-meta/route.ts +++ b/src/app/api/repos/[owner]/[name]/pulls-meta/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb } from '@/lib/db'; import { buildEtag, etagNotModified, withEtagHeaders } from '@/lib/etag'; @@ -9,6 +10,8 @@ export async function GET( ctx: { params: Promise<{ owner: string; name: string }> } ) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const full = `${params.owner}/${params.name}`; const db = getReadDb(); const url = new URL(req.url); diff --git a/src/app/api/repos/[owner]/[name]/pulls/route.ts b/src/app/api/repos/[owner]/[name]/pulls/route.ts index e4fdef1..e3eb358 100644 --- a/src/app/api/repos/[owner]/[name]/pulls/route.ts +++ b/src/app/api/repos/[owner]/[name]/pulls/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getReadDb, PullRow } from '@/lib/db'; import { refreshPullsIfStale } from '@/lib/refresh'; import { buildEtag, etagNotModified, withEtagHeaders } from '@/lib/etag'; @@ -27,6 +28,8 @@ export async function GET( ) { const params = await ctx.params; const { owner, name } = params; + const denied = await assertTrackedRepo(owner, name); + if (denied) return denied; const full = `${owner}/${name}`; // Poller handles refresh on its own cadence — see the same note in the diff --git a/src/app/api/repos/[owner]/[name]/validations/[number]/route.ts b/src/app/api/repos/[owner]/[name]/validations/[number]/route.ts index 3009fc8..e52dc98 100644 --- a/src/app/api/repos/[owner]/[name]/validations/[number]/route.ts +++ b/src/app/api/repos/[owner]/[name]/validations/[number]/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { assertTrackedRepo } from '@/lib/assert-tracked-repo'; import { getDb } from '@/lib/db'; import { getSessionFromCookies } from '@/lib/auth'; @@ -15,6 +16,8 @@ export async function POST( ctx: { params: Promise<{ owner: string; name: string; number: string }> }, ) { const params = await ctx.params; + const denied = await assertTrackedRepo(params.owner, params.name); + if (denied) return denied; const session = await getSessionFromCookies(); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });