Skip to content
Closed
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
8 changes: 3 additions & 5 deletions web/src/app/api/activity/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
InvalidActivityCursorError,
} from "./query";
import { parseLimit } from "@/lib/parse-limit";
import { internalError, badRequest } from "@/lib/api-response";

export const dynamic = "force-dynamic";

Expand All @@ -21,7 +22,7 @@ export async function GET(request: Request) {
decodeActivityCursor(cursor);
} catch (error) {
if (error instanceof InvalidActivityCursorError) {
return Response.json({ error: "Invalid cursor" }, { status: 400 });
return badRequest(request, "Invalid cursor");
}
throw error;
}
Expand All @@ -40,9 +41,6 @@ export async function GET(request: Request) {

return Response.json({ stats, transactions, nextCursor });
} catch {
return Response.json(
{ error: "An unexpected error occurred" },
{ status: 500 },
);
return internalError(request);
}
}
13 changes: 7 additions & 6 deletions web/src/app/api/dashboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export async function GET(req: NextRequest) {
let patronTalosIds = [];
try {
patronTalosIds = await withTimeout(
db.select({ talosId: tlsPatrons.talosId }).from(tlsPatrons).where(eq(tlsPatrons.stellarPublicKey, addr)),
db.select({ talosId: tlsPatrons.talosId }).from(tlsPatrons).where(eq(tlsPatrons.stellarPublicKey, addr)).limit(100),
25000,
"Dashboard patron query timeout",
);
} catch (error) {
if (error instanceof TimeoutError) {
return NextResponse.json(
{ error: "Query timeout. Please try again with a simpler query.", details: error.message },
{ error: "Query timeout. Please try again with a simpler query." },
{ status: 408 },
);
}
Expand All @@ -51,11 +51,12 @@ export async function GET(req: NextRequest) {
talosRows = await withTimeout(
db.query.tlsTalos.findMany({
where: whereCondition,
limit: 100,
with: {
approvals: { orderBy: (a, { desc: d }) => [d(a.createdAt)] },
approvals: { orderBy: (a, { desc: d }) => [d(a.createdAt)], limit: 50 },
activities: { orderBy: (a, { desc: d }) => [d(a.createdAt)], limit: 10 },
revenues: { orderBy: (r, { desc: d }) => [d(r.createdAt)] },
patrons: true,
revenues: { orderBy: (r, { desc: d }) => [d(r.createdAt)], limit: 50 },
patrons: { limit: 50 },
},
}),
30000,
Expand All @@ -64,7 +65,7 @@ export async function GET(req: NextRequest) {
} catch (error) {
if (error instanceof TimeoutError) {
return NextResponse.json(
{ error: "Query timeout. Please try again with a simpler query.", details: error.message },
{ error: "Query timeout. Please try again with a simpler query." },
{ status: 408 },
);
}
Expand Down
14 changes: 11 additions & 3 deletions web/src/app/api/ecosystem-intelligence/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
suppressSparseRecord,
suppressSparseRows,
} from "@/lib/analytics-privacy";
import { parseAnalyticsLimit } from "@/lib/analytics-limits";

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -109,6 +110,13 @@ export async function GET(req: Request) {
// if (!wallet) {
// return NextResponse.json({ error: "wallet parameter required" }, { status: 400 });
// }

const url = new URL(req.url);
const parsedLimit = parseAnalyticsLimit(url.searchParams.get("limit"), 10, 50);
if (!parsedLimit.ok) {
return parsedLimit.response;
}
const detailedLimit = parsedLimit.limit;

try {
const now = new Date();
Expand Down Expand Up @@ -309,7 +317,7 @@ export async function GET(req: Request) {
);
const byAgent = visibleByAgent.rows
.sort((a, b) => b.completionRate - a.completionRate)
.slice(0, 10);
.slice(0, detailedLimit);

// Opportunity metrics
// Calculate demand vs supply by category
Expand Down Expand Up @@ -351,7 +359,7 @@ export async function GET(req: Request) {
row => categoryCohortSizes[row.category] ?? 0,
);
const underservedCategories = visibleUnderservedCategories.rows
.slice(0, 5);
.slice(0, Math.min(detailedLimit, 5));

// Trending agents (by revenue growth)
const agentRevenue7d = new Map<string, { total: number; events: number }>();
Expand Down Expand Up @@ -383,7 +391,7 @@ export async function GET(req: Request) {
row => row.cohortSize,
);
const trendingAgents = visibleTrendingAgents.rows
.slice(0, 5)
.slice(0, Math.min(detailedLimit, 5))
.map(agent => ({
agentId: agent.agentId,
agentName: agent.agentName,
Expand Down
1 change: 1 addition & 0 deletions web/src/app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { db } from "@/db";
import { tlsTalos, tlsPatrons, tlsActivities, tlsRevenues } from "@/db/schema";
import { and, desc, eq, sql } from "drizzle-orm";
import { parseLimit } from "@/lib/parse-limit";
import { internalError, badRequest } from "@/lib/api-response";

// GET /api/leaderboard — Ranking data with cursor-based pagination
export async function GET(request: NextRequest) {
Expand Down
12 changes: 9 additions & 3 deletions web/src/app/api/proposals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { db } from "@/db";
import { tlsApprovals, tlsTalos } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
import { internalError } from "@/lib/api-response";
import { parseAnalyticsLimit } from "@/lib/analytics-limits";

// GET /api/proposals — All proposals across all Talos, newest first
// Optional ?status=pending|approved|rejected filter
// Optional ?status=pending|approved|rejected filter, optional ?limit=1..100 (default 50)
export async function GET(request: NextRequest) {
const status = new URL(request.url).searchParams.get("status");
const searchParams = new URL(request.url).searchParams;
const status = searchParams.get("status");
const parsedLimit = parseAnalyticsLimit(searchParams.get("limit"), 50, 100);
if (!parsedLimit.ok) return parsedLimit.response;
const limit = parsedLimit.limit;

try {
const rows = await db
Expand All @@ -28,7 +33,8 @@ export async function GET(request: NextRequest) {
.from(tlsApprovals)
.innerJoin(tlsTalos, eq(tlsApprovals.talosId, tlsTalos.id))
.where(status ? eq(tlsApprovals.status, status) : undefined)
.orderBy(desc(tlsApprovals.createdAt));
.orderBy(desc(tlsApprovals.createdAt))
.limit(limit);

return Response.json(rows);
} catch {
Expand Down
1 change: 1 addition & 0 deletions web/src/app/api/services/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { and, desc, eq, ilike, lt, ne, or } from "drizzle-orm";
import { parseLimit } from "@/lib/parse-limit";
import { fetchReputations } from "@/lib/reputation-ledger";
import { withTraceContext } from "@/lib/tracing";
import { internalError } from "@/lib/api-response";

// GET /api/services — Discover available services across all TALOS agents
async function handleGet(request: NextRequest) {
Expand Down
12 changes: 9 additions & 3 deletions web/src/app/api/talos/[id]/dividends/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { desc, eq } from "drizzle-orm";
import { verifyAgentApiKey } from "@/lib/auth";
import { recordDividendSchema, parseBody } from "@/lib/schemas";
import { emitWebhookEvent } from "@/lib/webhooks/delivery";
import { parseAnalyticsLimit } from "@/lib/analytics-limits";

/**
* GET /api/talos/:id/dividends
Expand All @@ -13,15 +14,20 @@ import { emitWebhookEvent } from "@/lib/webhooks/delivery";
* revenue that has been shared out to Mitos/Pulse token holders over time.
*
* Public read (consistent with revenue history + RLS anon_read policy).
* Returns the most recent 50 distributions, newest first.
* Returns distributions with bounded limit (default 50, max 100), newest first.
*/
export async function GET(
_request: NextRequest,
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;

try {
const { searchParams } = new URL(request.url);
const parsedLimit = parseAnalyticsLimit(searchParams.get("limit"), 50, 100);
if (!parsedLimit.ok) return parsedLimit.response;
const limit = parsedLimit.limit;

const talos = await db
.select({ id: tlsTalos.id })
.from(tlsTalos)
Expand All @@ -38,7 +44,7 @@ export async function GET(
.from(tlsDividends)
.where(eq(tlsDividends.talosId, id))
.orderBy(desc(tlsDividends.createdAt))
.limit(50);
.limit(limit);

return Response.json(dividends);
} catch {
Expand Down
39 changes: 39 additions & 0 deletions web/src/app/api/talos/[id]/exposure/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,43 @@ describe("GET /api/talos/:id/exposure", () => {
expect(body.exposures[0].settledAmount).toBe(100);
expect(body.windowDays).toBe(7);
});

it("returns 400 when limit exceeds maximum allowed limit", async () => {
mocks.verifyAgentApiKey.mockResolvedValue({ ok: true, talos: { id: "agent-1", apiKey: "valid" } });

const response = await GET(new NextRequest("http://localhost/api/talos/agent-1/exposure?limit=150"), {
params: Promise.resolve({ id: "agent-1" }),
});

expect(response.status).toBe(400);
const body = await response.json();
expect(body.error).toContain("exceeds maximum allowed limit of 100");
});

it.each(["0", "-1", "abc", "1.5", ""])("returns 400 for malformed limit=%s", async (val) => {
mocks.verifyAgentApiKey.mockResolvedValue({ ok: true, talos: { id: "agent-1", apiKey: "valid" } });

const response = await GET(new NextRequest(`http://localhost/api/talos/agent-1/exposure?limit=${val}`), {
params: Promise.resolve({ id: "agent-1" }),
});

expect(response.status).toBe(400);
const body = await response.json();
expect(body.error).toBe("limit must be a positive integer");
});

it("accepts maximum limit of 100", async () => {
mocks.verifyAgentApiKey.mockResolvedValue({ ok: true, talos: { id: "agent-1", apiKey: "valid" } });
mocks.select
.mockReturnValueOnce(chain([{ id: "agent-1" }]))
.mockReturnValueOnce(chain([]));

const response = await GET(new NextRequest("http://localhost/api/talos/agent-1/exposure?limit=100"), {
params: Promise.resolve({ id: "agent-1" }),
});

expect(response.status).toBe(200);
const body = await response.json();
expect(body.pagination.limit).toBe(100);
});
});
56 changes: 56 additions & 0 deletions web/src/app/api/talos/[id]/exposure/alerts/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,60 @@ describe("GET /api/talos/:id/exposure/alerts", () => {
expect(body.alerts.some((alert: { type: string }) => alert.type === "repeated-denial")).toBe(true);
expect(body.alerts.some((alert: { type: string }) => alert.type === "reconciliation-drift")).toBe(true);
});

it("returns 400 when limit exceeds maximum allowed limit of 100", async () => {
mocks.verifyAgentApiKey.mockResolvedValue({ ok: true, talos: { id: "agent-1", apiKey: "valid" } });

const response = await GET(new NextRequest("http://localhost/api/talos/agent-1/exposure/alerts?limit=150"), {
params: Promise.resolve({ id: "agent-1" }),
});

expect(response.status).toBe(400);
const body = await response.json();
expect(body.error).toContain("exceeds maximum allowed limit of 100");
});

it.each(["0", "-1", "abc", "1.5", ""])("returns 400 for malformed limit=%s", async (val) => {
mocks.verifyAgentApiKey.mockResolvedValue({ ok: true, talos: { id: "agent-1", apiKey: "valid" } });

const response = await GET(new NextRequest(`http://localhost/api/talos/agent-1/exposure/alerts?limit=${val}`), {
params: Promise.resolve({ id: "agent-1" }),
});

expect(response.status).toBe(400);
const body = await response.json();
expect(body.error).toBe("limit must be a positive integer");
});

it("bounds returned alerts array to requested limit", async () => {
mocks.verifyAgentApiKey.mockResolvedValue({ ok: true, talos: { id: "agent-1", apiKey: "valid" } });
mocks.select
.mockReturnValueOnce(chain([{ id: "agent-1" }]))
.mockReturnValueOnce(chain([
{
counterpartyId: "counterparty-1",
asset: "USDC",
reservedAmount: "1200",
settledAmount: "100",
deniedCount: 3,
lastObservedAt: new Date("2026-07-25T00:00:00.000Z"),
},
{
counterpartyId: "counterparty-2",
asset: "USDC",
reservedAmount: "2000",
settledAmount: "100",
deniedCount: 5,
lastObservedAt: new Date("2026-07-25T00:00:00.000Z"),
},
]));

const response = await GET(new NextRequest("http://localhost/api/talos/agent-1/exposure/alerts?limit=2"), {
params: Promise.resolve({ id: "agent-1" }),
});

expect(response.status).toBe(200);
const body = await response.json();
expect(body.alerts.length).toBeLessThanOrEqual(2);
});
});
12 changes: 9 additions & 3 deletions web/src/app/api/talos/[id]/exposure/alerts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { and, desc, eq, gte, sql } from "drizzle-orm";
import { db } from "@/db";
import { tlsCommerceJobs, tlsCommerceServices, tlsTalos } from "@/db/schema";
import { verifyAgentApiKey } from "@/lib/auth";
import { parseAnalyticsLimit } from "@/lib/analytics-limits";

function toNumber(value: unknown): number {
const parsed = typeof value === "string" ? Number(value) : Number(value ?? 0);
Expand All @@ -25,6 +26,11 @@ export async function GET(
const auth = await verifyAgentApiKey(request, id, ["revenue:read"]);
if (!auth.ok) return auth.response;

const { searchParams } = new URL(request.url);
const parsedLimit = parseAnalyticsLimit(searchParams.get("limit"), 25, 100);
if (!parsedLimit.ok) return parsedLimit.response;
const limit = parsedLimit.limit;

const talos = await db
.select({ id: tlsTalos.id })
.from(tlsTalos)
Expand All @@ -36,7 +42,6 @@ export async function GET(
return Response.json({ error: "TALOS not found" }, { status: 404 });
}

const { searchParams } = new URL(request.url);
const windowDays = Math.max(1, Number(searchParams.get("window") ?? 30));
const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000);

Expand All @@ -59,7 +64,8 @@ export async function GET(
)
.where(and(eq(tlsCommerceJobs.talosId, id), gte(tlsCommerceJobs.createdAt, since)))
.groupBy(tlsCommerceJobs.requesterTalosId, tlsCommerceServices.currency)
.orderBy(desc(sql`max(${tlsCommerceJobs.createdAt})`));
.orderBy(desc(sql`max(${tlsCommerceJobs.createdAt})`))
.limit(1000);

const alerts = [] as Array<{
type: string;
Expand Down Expand Up @@ -118,7 +124,7 @@ export async function GET(
return Response.json({
agentId: id,
windowDays,
alerts,
alerts: alerts.slice(0, limit),
});
} catch {
return Response.json({ error: "Internal server error" }, { status: 500 });
Expand Down
Loading