From 2b4b4cf9093a38b2d1507dafe8c234b1766f23d0 Mon Sep 17 00:00:00 2001 From: peace node Date: Wed, 9 Sep 2026 16:47:24 -0400 Subject: [PATCH 1/3] feat(history): add first-party asset previews and schema-driven details --- app/api/admin/runs/[id]/route.ts | 3 +- app/api/assets/[id]/route.ts | 127 + app/api/console/runs/[id]/route.ts | 12 +- app/layout.tsx | 3 +- components/admin/RunsPreview.tsx | 1 + components/console/CallDetailDrawer.tsx | 1466 +++- components/console/CallsTable.tsx | 25 +- components/console/ModalityChip.tsx | 19 + components/design-system/Tooltip.tsx | 158 - components/ui/tooltip.tsx | 56 + lib/assets/public.test.ts | 32 + lib/assets/public.ts | 91 + lib/console/activity-assets.ts | 3 +- lib/console/capability-modality.test.ts | 36 + lib/console/capability-modality.ts | 58 +- lib/console/dev-mock.ts | 550 +- lib/console/usage-capability-display.ts | 19 +- lib/mcp/fal-input-schema.test.ts | 116 + lib/mcp/fal-input-schema.ts | 258 + lib/mcp/store.ts | 15 +- lib/runs/execute.ts | 29 +- lib/runs/outputs.ts | 3 + lib/runs/types.ts | 23 + package.json | 1 + pnpm-lock.yaml | 6673 ++++++++++++------- tests/contracts/admin-runs-preview.test.tsx | 9 +- tests/contracts/asset-proxy.test.ts | 78 + tests/contracts/call-detail-media.test.tsx | 351 + tests/contracts/run-execution.test.ts | 52 + tests/contracts/run-security.test.ts | 6 + tests/integration/mcp-assets.test.ts | 2 +- 31 files changed, 7292 insertions(+), 2983 deletions(-) create mode 100644 app/api/assets/[id]/route.ts create mode 100644 components/console/ModalityChip.tsx delete mode 100644 components/design-system/Tooltip.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 lib/assets/public.test.ts create mode 100644 lib/assets/public.ts create mode 100644 lib/mcp/fal-input-schema.test.ts create mode 100644 lib/mcp/fal-input-schema.ts create mode 100644 tests/contracts/asset-proxy.test.ts create mode 100644 tests/contracts/call-detail-media.test.tsx diff --git a/app/api/admin/runs/[id]/route.ts b/app/api/admin/runs/[id]/route.ts index c2fa3724..2f1acd3d 100644 --- a/app/api/admin/runs/[id]/route.ts +++ b/app/api/admin/runs/[id]/route.ts @@ -1,6 +1,7 @@ import { getAdminPrincipal } from "@/lib/admin/auth"; import { getAdminRun } from "@/lib/runs/store"; import { runError, RUN_HEADERS } from "@/lib/runs/http"; +import { publicRunDetail } from "@/lib/assets/public"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET( @@ -17,7 +18,7 @@ export async function GET( const { id } = await context.params; const result = await getAdminRun(actor, id); if (!result) throw new Error("run_not_found"); - return Response.json(result, { headers: RUN_HEADERS }); + return Response.json(publicRunDetail(result), { headers: RUN_HEADERS }); } catch (error) { return runError(error); } diff --git a/app/api/assets/[id]/route.ts b/app/api/assets/[id]/route.ts new file mode 100644 index 00000000..fb18b2f0 --- /dev/null +++ b/app/api/assets/[id]/route.ts @@ -0,0 +1,127 @@ +import { lookup } from "node:dns/promises"; +import { isIP } from "node:net"; +import { getAssetSource } from "@/lib/mcp/store"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const FORWARDED_HEADERS = [ + "accept-ranges", + "content-length", + "content-range", + "content-type", + "etag", + "last-modified", +] as const; + +function isPrivateIp(address: string): boolean { + const normalized = address.toLowerCase().replace(/^::ffff:/, ""); + if (isIP(normalized) === 4) { + const [a, b] = normalized.split(".").map(Number); + return ( + a === 0 || + a === 10 || + a === 127 || + (a === 169 && b === 254) || + (a === 172 && b >= 16 && b <= 31) || + (a === 192 && b === 168) || + a >= 224 + ); + } + return ( + normalized === "::" || + normalized === "::1" || + normalized.startsWith("fc") || + normalized.startsWith("fd") || + /^fe[89ab]/.test(normalized) + ); +} + +async function assertPublicHttps(raw: string): Promise { + const url = new URL(raw); + if ( + url.protocol !== "https:" || + url.port || + url.username || + url.password || + url.hostname === "localhost" || + url.hostname.endsWith(".local") + ) { + throw new Error("unsafe_asset_origin"); + } + const addresses = await lookup(url.hostname, { all: true, verbatim: true }); + if ( + !addresses.length || + addresses.some(({ address }) => isPrivateIp(address)) + ) { + throw new Error("unsafe_asset_origin"); + } + return url; +} + +async function proxy(request: Request, id: string): Promise { + if (!/^[A-Za-z0-9_-]{1,160}$/.test(id)) { + return new Response("Not found", { status: 404 }); + } + const asset = await getAssetSource(id); + if (!asset) return new Response("Not found", { status: 404 }); + + try { + let target = await assertPublicHttps(asset.url); + let upstream: Response | undefined; + for (let redirects = 0; redirects <= 3; redirects += 1) { + upstream = await fetch(target, { + method: request.method, + redirect: "manual", + credentials: "omit", + cache: "no-store", + signal: AbortSignal.timeout(30_000), + headers: { + Accept: request.headers.get("accept") ?? "*/*", + "Accept-Encoding": "identity", + ...(request.headers.get("range") + ? { Range: request.headers.get("range")! } + : {}), + }, + }); + if (![301, 302, 303, 307, 308].includes(upstream.status)) break; + const location = upstream.headers.get("location"); + await upstream.body?.cancel(); + if (!location || redirects === 3) throw new Error("asset_redirect"); + target = await assertPublicHttps(new URL(location, target).href); + } + if (!upstream) throw new Error("asset_unavailable"); + const headers = new Headers({ + "cache-control": "public, max-age=300, stale-while-revalidate=86400", + "content-security-policy": "default-src 'none'; sandbox", + "x-content-type-options": "nosniff", + }); + for (const name of FORWARDED_HEADERS) { + const value = upstream.headers.get(name); + if (value) headers.set(name, value); + } + return new Response(request.method === "HEAD" ? null : upstream.body, { + status: upstream.status, + headers, + }); + } catch { + return new Response("Asset unavailable", { + status: 502, + headers: { "cache-control": "no-store" }, + }); + } +} + +export async function GET( + request: Request, + context: { params: Promise<{ id: string }> } +) { + return proxy(request, (await context.params).id); +} + +export async function HEAD( + request: Request, + context: { params: Promise<{ id: string }> } +) { + return proxy(request, (await context.params).id); +} diff --git a/app/api/console/runs/[id]/route.ts b/app/api/console/runs/[id]/route.ts index 10b55901..ea00e203 100644 --- a/app/api/console/runs/[id]/route.ts +++ b/app/api/console/runs/[id]/route.ts @@ -1,5 +1,10 @@ import { getOwnRun } from "@/lib/runs/store"; import { requireRunOwner, runError, RUN_HEADERS } from "@/lib/runs/http"; +import { publicRunDetail } from "@/lib/assets/public"; +import { + loadFalInputSchema, + resolveFalCatalogEntry, +} from "@/lib/mcp/fal-input-schema"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET( @@ -11,7 +16,12 @@ export async function GET( const { id } = await context.params; const result = await getOwnRun(owner, id); if (!result) throw new Error("run_not_found"); - return Response.json(result, { headers: RUN_HEADERS }); + const catalog = resolveFalCatalogEntry(result); + const inputSchema = catalog ? await loadFalInputSchema(catalog) : null; + return Response.json( + { ...publicRunDetail(result), inputSchema }, + { headers: RUN_HEADERS } + ); } catch (error) { return runError(error); } diff --git a/app/layout.tsx b/app/layout.tsx index cd0eed2c..d613f9b9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import type { CSSProperties } from "react"; import { Toaster } from "sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; import "./globals.css"; const SITE_TITLE = "Livepeer Early Access"; @@ -62,7 +63,7 @@ export default function RootLayout({