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
3 changes: 2 additions & 1 deletion app/api/admin/runs/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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);
}
Expand Down
127 changes: 127 additions & 0 deletions app/api/assets/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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<URL> {
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<Response> {
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 });

Comment on lines +62 to +68
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);
}
12 changes: 11 additions & 1 deletion app/api/console/runs/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -62,7 +63,7 @@ export default function RootLayout({
<script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
</head>
<body className="min-h-screen bg-background font-sans text-foreground antialiased">
{children}
<TooltipProvider>{children}</TooltipProvider>
<Toaster
position="bottom-center"
theme="system"
Expand Down
1 change: 1 addition & 0 deletions components/admin/RunsPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export default function RunsPreview() {
</section>
</div>
<CallDetailDrawer
variant="admin"
row={selectedRow}
rows={rows}
open={!!selected}
Expand Down
Loading
Loading