Skip to content
Open
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
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
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
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
19 changes: 9 additions & 10 deletions web/src/app/api/talos/[id]/exposure/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 parseWindow(windowParam: string | null): { windowMs: number; windowDays: number } {
const raw = windowParam ?? "30d";
Expand All @@ -13,13 +14,6 @@ function parseWindow(windowParam: string | null): { windowMs: number; windowDays
return { windowMs: windowDays * 24 * 60 * 60 * 1000, windowDays };
}

function parsePagination(request: Request): { limit: number; cursor: string | null } {
const { searchParams } = new URL(request.url);
const limit = Math.min(Math.max(parseInt(searchParams.get("limit") ?? "25", 10) || 25, 1), 100);
const cursor = searchParams.get("cursor");
return { limit, cursor };
}

function toNumber(value: unknown): number {
const parsed = typeof value === "string" ? Number(value) : Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
Expand All @@ -41,6 +35,12 @@ 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 cursor = searchParams.get("cursor");

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

const { searchParams } = new URL(request.url);
const { windowMs, windowDays } = parseWindow(searchParams.get("window"));
const { limit, cursor } = parsePagination(request);
const since = new Date(Date.now() - windowMs);

const rows = await db
Expand All @@ -78,7 +76,8 @@ export async function GET(
)
.where(and(eq(tlsCommerceJobs.talosId, id), gte(tlsCommerceJobs.createdAt, since)))
.groupBy(tlsCommerceJobs.requesterTalosId, tlsCommerceJobs.serviceName, tlsCommerceServices.currency)
.orderBy(desc(sql`max(${tlsCommerceJobs.createdAt})`));
.orderBy(desc(sql`max(${tlsCommerceJobs.createdAt})`))
.limit(1000);

const filtered = rows
.map((row) => ({
Expand Down
5 changes: 3 additions & 2 deletions web/src/app/api/talos/[id]/financial-projection/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export async function GET(
const patrons = await db
.select()
.from(tlsPatrons)
.where(eq(tlsPatrons.talosId, id));
.where(eq(tlsPatrons.talosId, id))
.limit(100);

// Calculate revenue summary
const totalRevenue = revenues.reduce(
Expand Down Expand Up @@ -232,7 +233,7 @@ Generate projections for the next 12 months.`;
} catch (error) {
console.error("Financial projection error:", error);
return Response.json(
{ error: "Internal server error", details: error instanceof Error ? error.message : "Unknown error" },
{ error: "Internal server error" },
{ status: 500 }
);
}
Expand Down
Loading