From bc6bd53a5872fa43fb3af6185c692aa4b468095b Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Wed, 9 Sep 2026 19:18:10 -0400 Subject: [PATCH 01/26] fix(history): join orchestrator 8-hex tickets onto MCP job_* Cost Production usage ids are CloudEvent 8-hex, not console job_* keys. Keep the current-month ticket feed and match by capability and time so the request drawer can show a real fee. --- app/api/pymthouse/account-requests/route.ts | 1 + components/console/CallsSection.tsx | 58 +++++++++----- lib/console/activity-output-match.test.ts | 78 ++++++++++++++++++- lib/console/activity-output-match.ts | 69 ++++++++++++++++ lib/console/pymthouse-bff.ts | 14 +++- lib/console/run-activity.test.ts | 4 +- lib/console/run-activity.ts | 6 +- lib/console/signed-ticket-activity.ts | 1 + lib/console/types.ts | 2 + tests/contracts/account-history-route.test.ts | 7 ++ tests/contracts/account-history.test.ts | 19 +++++ tests/contracts/home-history-surface.test.tsx | 68 +++++++++++++++- 12 files changed, 296 insertions(+), 31 deletions(-) diff --git a/app/api/pymthouse/account-requests/route.ts b/app/api/pymthouse/account-requests/route.ts index f79d2150..da90ab9b 100644 --- a/app/api/pymthouse/account-requests/route.ts +++ b/app/api/pymthouse/account-requests/route.ts @@ -53,6 +53,7 @@ export async function GET(request: NextRequest) { email: session.email, cursor: next, limit, + ...(includeCorrelated ? { recentWindow: true } : {}), }); if ( payload.externalUserId !== session.externalUserId || diff --git a/components/console/CallsSection.tsx b/components/console/CallsSection.tsx index d4bc8c7d..e94d26bf 100644 --- a/components/console/CallsSection.tsx +++ b/components/console/CallsSection.tsx @@ -6,6 +6,7 @@ import SectionHeader from "@/components/console/SectionHeader"; import CallsTable from "@/components/console/CallsTable"; import CallDetailDrawer from "@/components/console/CallDetailDrawer"; import { useAuth } from "@/components/console/AuthContext"; +import { matchRunTicketFees } from "@/lib/console/activity-output-match"; import { useAccountRequests } from "@/lib/console/useAccountRequests"; import { useRunDetail, useRunHistory } from "@/lib/console/useRunHistory"; import { runToActivity } from "@/lib/console/run-activity"; @@ -28,23 +29,50 @@ export default function CallsSection({ }, ownerKey ); + const requestId = useSearchParams().get("request"); + const detail = useRunDetail( + "/api/console/runs", + requestId, + ownerKey, + isConnected + ); // Correlate billing receipts with saved runs; billing is not a second history feed. const billing = useAccountRequests(isConnected, ownerKey, true); const billingRows = billing.status === "ready" ? billing.rows : null; const feeByGateway = useMemo(() => { - const fees = new Map(); - if (!billingRows) return fees; - for (const row of billingRows) { - if (!row.gatewayRequestId || row.costDisplay === "—") continue; - fees.set(row.gatewayRequestId, { - costDisplay: row.costDisplay, - ...(row.costExact ? { costExact: row.costExact } : {}), - }); - } - return fees; - }, [billingRows]); + if (!billingRows) return new Map(); + const tickets = billingRows.flatMap((row) => + row.gatewayRequestId && row.costDisplay !== "—" + ? [ + { + gatewayRequestId: row.gatewayRequestId, + modelId: row.capabilityId ?? "", + time: row.timestamp, + costDisplay: row.costDisplay, + ...(row.costExact ? { costExact: row.costExact } : {}), + }, + ] + : [] + ); + const runs = [ + ...(history.page?.items ?? []).map((run) => ({ + gatewayRequestId: run.gatewayRequestId, + capability: run.modelId ?? run.capability, + createdAt: run.createdAt, + })), + ...(detail.detail + ? [ + { + gatewayRequestId: detail.detail.gatewayRequestId, + capability: detail.detail.modelId ?? detail.detail.capability, + createdAt: detail.detail.createdAt, + }, + ] + : []), + ]; + return matchRunTicketFees(runs, tickets); + }, [billingRows, history.page, detail.detail]); const router = useRouter(); - const requestId = useSearchParams().get("request"); const recorded = useMemo( () => history.page?.items.map((run) => @@ -56,12 +84,6 @@ export default function CallsSection({ const found = rows.find( (row) => row.id === requestId || row.gatewayRequestId === requestId ); - const detail = useRunDetail( - "/api/console/runs", - requestId, - ownerKey, - isConnected - ); const openRow = detail.detail && (detail.detail.id === requestId || diff --git a/lib/console/activity-output-match.test.ts b/lib/console/activity-output-match.test.ts index 0988dc35..ad7dd93e 100644 --- a/lib/console/activity-output-match.test.ts +++ b/lib/console/activity-output-match.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { matchTicketOutputs } from "./activity-output-match"; +import { matchRunTicketFees, matchTicketOutputs } from "./activity-output-match"; import type { SignedTicketRequestRow } from "./account-usage"; function ticket( @@ -163,3 +163,79 @@ test("job_* tickets without an exact id match do not fuzzy-join another job", () ]); assert.equal(matched.has("job_abc"), false); }); + +test("exact ticket id still prices a run", () => { + const fees = matchRunTicketFees( + [ + { + gatewayRequestId: "job_saved", + capability: "livepeer-example/fal-flux-schnell", + createdAt: "2026-09-09T21:09:30.000Z", + }, + ], + [ + { + gatewayRequestId: "job_saved", + modelId: "livepeer-example/fal-flux-schnell", + time: "2026-09-09T21:09:30.000Z", + costDisplay: "$0.0030", + costExact: "$0.002999", + }, + ] + ); + assert.equal(fees.get("job_saved")?.costDisplay, "$0.0030"); +}); + +test("orchestrator 8-hex tickets price MCP job_* runs by capability and nearest time", () => { + const fees = matchRunTicketFees( + [ + { + gatewayRequestId: "job_b6edf64e2a2442da", + capability: "livepeer-example/fal-flux-schnell", + createdAt: "2026-09-09T21:09:28.000Z", + }, + { + gatewayRequestId: "job_later", + capability: "livepeer-example/fal-flux-schnell", + createdAt: "2026-09-09T21:15:16.000Z", + }, + ], + [ + { + gatewayRequestId: "5b66062c", + modelId: "livepeer-example/fal-flux-schnell", + time: "2026-09-09T21:09:30.000Z", + costDisplay: "$0.0030", + }, + { + gatewayRequestId: "55d0075d", + modelId: "livepeer-example/fal-flux-schnell", + time: "2026-09-09T21:15:16.000Z", + costDisplay: "$0.0030", + }, + ] + ); + assert.equal(fees.get("job_b6edf64e2a2442da")?.costDisplay, "$0.0030"); + assert.equal(fees.get("job_later")?.costDisplay, "$0.0030"); +}); + +test("8-hex tickets do not price a different capability", () => { + const fees = matchRunTicketFees( + [ + { + gatewayRequestId: "job_video", + capability: "livepeer-example/fal-ltx-25-t2v-fast", + createdAt: "2026-09-09T21:09:30.000Z", + }, + ], + [ + { + gatewayRequestId: "5b66062c", + modelId: "livepeer-example/fal-flux-schnell", + time: "2026-09-09T21:09:30.000Z", + costDisplay: "$0.0030", + }, + ] + ); + assert.equal(fees.has("job_video"), false); +}); diff --git a/lib/console/activity-output-match.ts b/lib/console/activity-output-match.ts index 96a3c160..584b586f 100644 --- a/lib/console/activity-output-match.ts +++ b/lib/console/activity-output-match.ts @@ -91,3 +91,72 @@ export function matchTicketOutputs( return out; } + +export type FeeTicket = { + gatewayRequestId: string; + modelId: string; + time: string; + costDisplay: string; + costExact?: string; +}; + +export type FeeRun = { + gatewayRequestId: string; + capability: string; + createdAt: string; +}; + +/** + * Join PymtHouse tickets onto console runs. + * Production tickets are orchestrator 8-hex CloudEvent ids; MCP runs store + * `job_*`. Exact id wins; leftover 8-hex tickets assign greedily to the + * nearest unused same-capability run in the asset-match window. + */ +export function matchRunTicketFees( + runs: FeeRun[], + tickets: FeeTicket[] +): Map { + const out = new Map(); + const used = new Set(); + const feeOf = (ticket: FeeTicket) => ({ + costDisplay: ticket.costDisplay, + ...(ticket.costExact ? { costExact: ticket.costExact } : {}), + }); + + for (const run of runs) { + const exact = tickets.find( + (ticket) => + ticket.gatewayRequestId === run.gatewayRequestId && + !used.has(ticket.gatewayRequestId) + ); + if (!exact || exact.costDisplay === "—") continue; + used.add(exact.gatewayRequestId); + out.set(run.gatewayRequestId, feeOf(exact)); + } + + const remaining = [...runs] + .filter((run) => !out.has(run.gatewayRequestId)) + .sort((a, b) => a.createdAt.localeCompare(b.createdAt)); + + for (const run of remaining) { + const runTime = Date.parse(run.createdAt); + if (!Number.isFinite(runTime) || !run.capability.trim()) continue; + let best: { ticket: FeeTicket; delta: number } | null = null; + for (const ticket of tickets) { + if (used.has(ticket.gatewayRequestId)) continue; + if (!isOrchestratorTicketId(ticket.gatewayRequestId)) continue; + if (ticket.modelId !== run.capability) continue; + if (ticket.costDisplay === "—") continue; + const ticketTime = Date.parse(ticket.time); + if (!Number.isFinite(ticketTime)) continue; + const delta = Math.abs(ticketTime - runTime); + if (delta > TICKET_ASSET_MATCH_WINDOW_MS) continue; + if (!best || delta < best.delta) best = { ticket, delta }; + } + if (!best) continue; + used.add(best.ticket.gatewayRequestId); + out.set(run.gatewayRequestId, feeOf(best.ticket)); + } + + return out; +} diff --git a/lib/console/pymthouse-bff.ts b/lib/console/pymthouse-bff.ts index a96350da..2fea64f4 100644 --- a/lib/console/pymthouse-bff.ts +++ b/lib/console/pymthouse-bff.ts @@ -254,6 +254,12 @@ export async function fetchAccountRequestsForExternalUser(input: { email?: string; cursor?: string | null; limit?: number; + /** + * Cost lookups must not send a 365-day window. OpenMeter lists cap at 100 + * events, so a year-long range drops the ticket Home is trying to price. + * Omitting from/to uses the current UTC month on `/me/usage/requests`. + */ + recentWindow?: boolean; }): Promise { const publicClientId = readPublicClientId(); const minted = await mintEndUserAccessToken( @@ -263,9 +269,11 @@ export async function fetchAccountRequestsForExternalUser(input: { const accessToken = minted.access_token; const url = new URL(`${issuerOriginFromConfig()}/api/v1/user/usage/requests`); - const range = historyRange(); - url.searchParams.set("from", range.from); - url.searchParams.set("to", range.to); + if (!input.recentWindow) { + const range = historyRange(); + url.searchParams.set("from", range.from); + url.searchParams.set("to", range.to); + } if (input.cursor) url.searchParams.set("cursor", input.cursor); if (input.limit != null) url.searchParams.set("limit", String(input.limit)); diff --git a/lib/console/run-activity.test.ts b/lib/console/run-activity.test.ts index 87800005..d49bb7b1 100644 --- a/lib/console/run-activity.test.ts +++ b/lib/console/run-activity.test.ts @@ -46,7 +46,7 @@ test("run history uses the signed-ticket fee mapper", () => { assert.equal(row.costExact, "$0.001"); }); -test("run detail reads the latest billing_usage event", () => { +test("run detail does not take Cost from Postgres run events", () => { const detail = { ...summary(), submittedArguments: null, @@ -73,5 +73,5 @@ test("run detail reads the latest billing_usage event", () => { const fields = feeFieldsFromRunEvents(detail.events); assert.equal(fields?.networkFeeUsdMicros, "2500"); const row = runToActivity(detail); - assert.equal(row.costDisplay, "$0.0025"); + assert.equal(row.costDisplay, "—"); }); diff --git a/lib/console/run-activity.ts b/lib/console/run-activity.ts index e9a4d850..0516b227 100644 --- a/lib/console/run-activity.ts +++ b/lib/console/run-activity.ts @@ -65,11 +65,7 @@ export function runToActivity( run.startedAt && run.completedAt ? Date.parse(run.completedAt) - Date.parse(run.startedAt) : null; - const cost = - costFromFee(fee) ?? - costFromFee( - feeFieldsFromRunEvents("events" in run ? run.events : undefined) - ); + const cost = costFromFee(fee); return { id: run.id, recordKind: "run", diff --git a/lib/console/signed-ticket-activity.ts b/lib/console/signed-ticket-activity.ts index 8ad3eb30..a24b40f3 100644 --- a/lib/console/signed-ticket-activity.ts +++ b/lib/console/signed-ticket-activity.ts @@ -31,6 +31,7 @@ export function mapSignedTicketToActivityRow( return { id: `usage:${row.eventId}`, gatewayRequestId: row.gatewayRequestId, + capabilityId: row.modelId, recordKind: "usage", environmentId: "env-production", timestamp: row.time, diff --git a/lib/console/types.ts b/lib/console/types.ts index cc40ec9f..c487fd6c 100644 --- a/lib/console/types.ts +++ b/lib/console/types.ts @@ -414,6 +414,8 @@ export type AccountActivityStatus = export interface AccountActivityRow { recordKind?: "run" | "usage"; gatewayRequestId?: string; + /** Raw capability id (`livepeer-example/fal-flux-schnell`). Used to join Cost. */ + capabilityId?: string; id: string; /** Environment this request ran under. Scopes Jobs + Home runs by env. */ environmentId: string; diff --git a/tests/contracts/account-history-route.test.ts b/tests/contracts/account-history-route.test.ts index 3c751c27..59dc302c 100644 --- a/tests/contracts/account-history-route.test.ts +++ b/tests/contracts/account-history-route.test.ts @@ -139,6 +139,13 @@ it("returns scoped matched receipts when Home explicitly requests correlation", expect(result.items).toEqual([row("owned")]); expect(result.nextCursor).toBe("next"); expect(recordRunUsage).toHaveBeenCalledTimes(1); + expect(fetchAccountRequestsForExternalUser).toHaveBeenCalledWith({ + externalUserId: "eu_fixture", + email: "fixture@example.invalid", + cursor: undefined, + limit: 50, + recentWindow: true, + }); expect(JSON.stringify(vi.mocked(recordRunUsage).mock.calls)).not.toContain( "event-other" ); diff --git a/tests/contracts/account-history.test.ts b/tests/contracts/account-history.test.ts index 95a6a75b..8b14e325 100644 --- a/tests/contracts/account-history.test.ts +++ b/tests/contracts/account-history.test.ts @@ -63,6 +63,25 @@ it("keeps old history and pagination even when its media is unavailable", async expect(url.searchParams.get("limit")).toBe("50"); }); +it("omits the year window for Cost so the current-month ticket feed is kept", async () => { + const fetch = vi.fn(async () => + Response.json({ + items: [], + nextCursor: null, + openMeterConfigured: true, + }) + ); + vi.stubGlobal("fetch", fetch); + await fetchAccountRequestsForExternalUser({ + externalUserId: "test-user", + limit: 50, + recentWindow: true, + }); + const url = new URL(fetch.mock.calls[0][0] as string); + expect(url.searchParams.get("from")).toBeNull(); + expect(url.searchParams.get("to")).toBeNull(); +}); + it("does not label History with media expiry or a seven-day limit", () => { const source = readFileSync("components/console/CallsSection.tsx", "utf8"); expect(source).toContain('title="History"'); diff --git a/tests/contracts/home-history-surface.test.tsx b/tests/contracts/home-history-surface.test.tsx index 2733e4a7..856bdceb 100644 --- a/tests/contracts/home-history-surface.test.tsx +++ b/tests/contracts/home-history-surface.test.tsx @@ -119,10 +119,74 @@ it("shows Postgres rows even when billing is unavailable", async () => { expect(screen.queryByRole("alert")).toBeNull(); expect(screen.queryByText("Usage-only history")).toBeNull(); await waitFor(() => - expect(screen.getByTestId("detail-cost").textContent).toBe("$0.0025") + expect(screen.getByTestId("detail-cost").textContent).toBe("—") ); }); +it("joins an orchestrator 8-hex ticket onto an MCP job_* run", async () => { + const run = { + id: "28d04c8a-7edd-487e-a0c4-5f95ed637a4b", + principalId: "external", + userId: "user", + externalAccountId: "account", + gatewayRequestId: "job_fe3e40004a49442c", + providerRequestId: null, + provider: null, + source: "mcp", + capability: "livepeer-example/fal-ideogram-v4", + modelId: "livepeer-example/fal-ideogram-v4", + endpoint: null, + status: "succeeded" as const, + captureVersion: 1, + errorCode: null, + errorMessage: null, + version: 2, + createdAt: "2026-09-09T21:42:18.000Z", + updatedAt: "2026-09-09T21:42:19.000Z", + startedAt: "2026-09-09T21:42:18.000Z", + completedAt: "2026-09-09T21:42:19.000Z", + email: null, + }; + records.push(run); + navigation.search = "request=28d04c8a-7edd-487e-a0c4-5f95ed637a4b"; + fetcher.mockImplementation(async (input: string) => { + if (input === "/api/console/runs/28d04c8a-7edd-487e-a0c4-5f95ed637a4b") { + return Response.json({ + ...run, + submittedArguments: null, + result: null, + captureRedactedPaths: [], + assets: [], + events: [], + }); + } + if (String(input).startsWith("/api/console/runs")) + return Response.json({ items: records, nextCursor: null }); + return Response.json({ + items: [ + { + eventId: "c9a1fae7", + gatewayRequestId: "c9a1fae7", + time: "2026-09-09T21:42:19.000Z", + clientId: "app_test", + externalUserId: "eu_test", + pipeline: "text-to-image", + modelId: "livepeer-example/fal-ideogram-v4", + networkFeeUsdMicros: "9984.675492933755", + feeWei: "4048746912830", + }, + ], + nextCursor: null, + openMeterConfigured: true, + }); + }); + render(); + await waitFor(() => + expect(screen.getByTestId("detail-cost").textContent).not.toBe("—") + ); + expect(screen.getByTestId("detail-cost").textContent).toMatch(/^\$0\.0/); +}); + it("joins a correlated ticket fee onto the saved run, without adding a billing row", async () => { records.push({ id: "saved-run", @@ -179,7 +243,7 @@ it("joins a correlated ticket fee onto the saved run, without adding a billing r }); render(); await screen.findByRole("button", { name: "Inspect saved-run" }); - expect(screen.getByText("$0.0010")).toBeTruthy(); + await waitFor(() => expect(screen.getByText("$0.0010")).toBeTruthy()); expect( fetcher.mock.calls.some(([url]) => String(url).includes("includeCorrelated=1") From 622260bc15f3fb895a0b2a0cd731095e3b038c95 Mon Sep 17 00:00:00 2001 From: peace node Date: Wed, 9 Sep 2026 16:47:24 -0400 Subject: [PATCH 02/26] 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({ ", { + headers: { "content-type": "image/svg+xml" }, + }) + ); + const fromUpstream = await GET(signedRequest(), context); + expect(fromUpstream.headers.get("content-type")).not.toBe("image/svg+xml"); + expect(fromUpstream.headers.get("content-security-policy")).toContain( + "sandbox" + ); + + vi.mocked(getAssetSource).mockResolvedValue({ + ...source, + mediaType: "image/svg+xml", + }); + vi.mocked(fetchPinnedAsset).mockImplementation( + async () => + new Response("", { + headers: { "content-type": "application/octet-stream" }, + }) + ); + const fromStored = await GET(signedRequest(), context); + expect(fromStored.headers.get("content-type")).not.toBe("image/svg+xml"); + expect(fromStored.headers.get("content-security-policy")).toContain( + "sandbox" + ); +}); + it("serves only owner-bound synthetic fixtures without widening the proxy host allowlist", async () => { vi.stubEnv("VERCEL_ENV", "preview"); vi.stubEnv("CONSOLE_PREVIEW_FIXTURES", "1"); diff --git a/tests/contracts/public-media.test.ts b/tests/contracts/public-media.test.ts index b7259f52..825b0f93 100644 --- a/tests/contracts/public-media.test.ts +++ b/tests/contracts/public-media.test.ts @@ -101,6 +101,32 @@ it("captures distinct explicit expiries, keeps availability guarantees separate, ).toBe(a); }); +it("strips compound provider URL keys including credentialed values", () => { + const token = "https://v3.fal.media/files/x?token=secret"; + const queue = "https://queue.fal.run/fal-ai/flux/requests/id/status"; + expect( + sanitizePublicMedia( + { + output_url: token, + outputUrl: token, + outputURL: token, + preview_url: token, + previewUrl: token, + download_url: token, + status_url: queue, + statusUrl: queue, + response_url: "https://queue.fal.run/fal-ai/flux/requests/id", + responseURI: queue, + asset2Url: token, + prompt: `see ${token}`, + website: "https://example.com", + }, + [], + "eu_test" + ) + ).toEqual({ prompt: `see ${token}`, website: "https://example.com" }); +}); + it("drops unsupported 3D media URLs without removing ordinary model identifiers", () => { expect( sanitizePublicMedia( diff --git a/tests/contracts/run-execution.test.ts b/tests/contracts/run-execution.test.ts index b2c65269..d36531c8 100644 --- a/tests/contracts/run-execution.test.ts +++ b/tests/contracts/run-execution.test.ts @@ -270,6 +270,60 @@ describe("durable MCP execution", () => { ); }); +it("retries accepted payment persist on a transient store failure", async () => { + const deps = fixture(); + let acceptedAttempts = 0; + vi.mocked(deps.store.recordRunPaymentManifest).mockImplementation( + async (_owner, _id, payment) => { + if (payment.phase === "accepted" && ++acceptedAttempts === 1) + throw new Error("db unavailable"); + } + ); + vi.mocked(deps.infer).mockImplementation(async ({ onPayment }) => { + await onPayment({ manifestId: "manifest-1", phase: "prepared" }); + await onPayment({ manifestId: "manifest-1", phase: "accepted" }); + return { + gatewayRequestId: "job_test", + data: { text: "ok" }, + status: "succeeded", + url: null, + billableUnits: null, + } as never; + }); + const reply = await executeDurableRun(principal, { capability: "test" }, deps); + expect(reply.isError).toBe(false); + expect(acceptedAttempts).toBe(2); + expect(deps.store.recordRunPaymentManifest).toHaveBeenCalledTimes(3); +}); + +it("aborts after payment persist retries are exhausted", async () => { + const deps = fixture(); + vi.mocked(deps.store.recordRunPaymentManifest).mockRejectedValue( + new Error("db unavailable") + ); + vi.mocked(deps.infer).mockImplementation(async ({ onPayment }) => { + await onPayment({ manifestId: "manifest-1", phase: "accepted" }); + return { + gatewayRequestId: "job_test", + data: { text: "ok" }, + status: "succeeded", + url: null, + billableUnits: null, + } as never; + }); + const reply = await executeDurableRun(principal, { capability: "test" }, deps); + expect(reply.isError).toBe(true); + expect(deps.store.recordRunPaymentManifest).toHaveBeenCalledTimes(3); + expect(deps.store.transitionRun).toHaveBeenCalledWith( + owner, + "run_test", + expect.objectContaining({ + status: "unknown", + errorCode: "execution_outcome_unknown", + }) + ); +}); + it("records every payment phase against the run even when inference fails afterward", async () => { const deps = fixture(); vi.mocked(deps.infer).mockImplementation(async ({ onPayment }) => { @@ -301,6 +355,15 @@ it("persists explicit expiry and sanitizes all returned media with partial captu { url: "https://provider.example/missing" }, { url: "https://provider.example/signed?token=private" }, ], + output_url: "https://provider.example/download?token=private", + outputUrl: "https://provider.example/download?token=private", + outputURL: "https://provider.example/download?token=private", + preview_url: "https://provider.example/preview?token=private", + previewUrl: "https://provider.example/preview?token=private", + status_url: "https://queue.fal.run/fal-ai/flux/requests/id/status", + statusUrl: "https://queue.fal.run/fal-ai/flux/requests/id/status", + responseURI: "https://queue.fal.run/fal-ai/flux/requests/id", + asset2Url: "https://provider.example/signed?token=private", }, } as unknown as Awaited>); vi.mocked(deps.store.transitionRun).mockResolvedValue({ @@ -328,7 +391,7 @@ it("persists explicit expiry and sanitizes all returned media with partial captu }) ); expect(JSON.stringify(result.payload)).not.toMatch( - /provider.example|private|REDACTED/ + /provider.example|private|REDACTED|queue\.fal\.run/ ); expect(JSON.stringify(result.payload)).toContain("/api/assets/owned"); }); From 39a5b97978e3de33e07042d91c44794945c2caf9 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Fri, 11 Sep 2026 13:09:00 -0400 Subject: [PATCH 19/26] chore: collapse drizzle snapshots in GitHub review (#81) Mark Drizzle Kit full-schema dumps as linguist-generated so #78 hides ~7.5k lines of snapshot JSON in Files changed. --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d71c301e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Drizzle Kit writes a full schema dump per migration, not a delta. +# Keep the files for generate/migrate; collapse them in GitHub review. +drizzle-baseline/meta/*_snapshot.json linguist-generated=true +drizzle/meta/*_snapshot.json linguist-generated=true From 3564112689975b501a67ba7bef3fc7cea98c452f Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Fri, 11 Sep 2026 13:21:11 -0400 Subject: [PATCH 20/26] feat(public): add public event metadata handling and sanitize URLs - Introduced a set of public event metadata keys to filter relevant metadata. - Implemented functions to sanitize event keys and metadata, ensuring sensitive URLs are stripped from public responses. - Updated the `publicRunDetail` function to include sanitized event details. - Added tests to verify the removal of sensitive URLs from event keys and metadata in public responses. --- lib/assets/public.ts | 53 ++++++++++++++++++++++ tests/contracts/public-media.test.ts | 61 +++++++++++++++++++++++++ tests/contracts/run-http.test.ts | 66 ++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) diff --git a/lib/assets/public.ts b/lib/assets/public.ts index da8cd19a..b043b03d 100644 --- a/lib/assets/public.ts +++ b/lib/assets/public.ts @@ -137,6 +137,48 @@ export function removeAssetUrls(value: JsonValue, urls: string[]): JsonValue { return visit(value) ?? null; } +const PUBLIC_EVENT_METADATA = new Set([ + "kind", + "eventId", + "billingEventId", + "ticketGatewayRequestId", + "networkFeeUsdMicros", + "feeWei", + "ethUsdPrice", + "pixels", + "pipeline", + "modelId", + "timestamp", + "providerStatus", + "phase", + "inferenceTimeMs", + "retryable", + "reason", +]); + +function publicEventKey(eventKey: string): string { + return eventKey + .replace(/[a-z][a-z0-9+.-]*:\/\/\S+/gi, "") + .replace(/:+$/g, "") + .replace(/:{2,}/g, ":"); +} + +function publicEventMetadata( + metadata: Record | undefined, + assets: Pick[], + principalId: string +): Record { + const picked = Object.fromEntries( + Object.entries(metadata ?? {}).filter(([key]) => + PUBLIC_EVENT_METADATA.has(key) + ) + ); + const clean = sanitizePublicMedia(picked, assets, principalId); + return clean && typeof clean === "object" && !Array.isArray(clean) + ? (clean as Record) + : {}; +} + /** Keep durable records while exposing only owner-bound first-party media. */ export function publicRunDetail(detail: RunDetail): RunDetail { return { @@ -161,5 +203,16 @@ export function publicRunDetail(detail: RunDetail): RunDetail { ), } : null, + events: (detail.events ?? []).map((event) => ({ + id: event.id, + eventKey: publicEventKey(event.eventKey), + status: event.status, + createdAt: event.createdAt, + metadata: publicEventMetadata( + event.metadata, + detail.assets, + detail.principalId + ), + })), }; } diff --git a/tests/contracts/public-media.test.ts b/tests/contracts/public-media.test.ts index 825b0f93..6440034c 100644 --- a/tests/contracts/public-media.test.ts +++ b/tests/contracts/public-media.test.ts @@ -50,6 +50,67 @@ it("rewrites owned media but removes unmatched media even with partial persisten "https://preview.example/api/assets/owned?exp=" ); }); +it("strips provider queue URLs from public event keys and metadata", () => { + const queue = "https://queue.fal.run/fal-ai/flux/requests/req-1/status"; + const clean = publicRunDetail({ + principalId: "eu_test", + billing: { networkFeeUsdMicros: "2982", manifestCount: 1 }, + assets: [], + submittedArguments: { prompt: "portrait" }, + result: { value: { text: "ok" } }, + events: [ + { + id: "evt_progress", + eventKey: `progress:IN_QUEUE:req-1:${queue}`, + status: "running", + createdAt: "2026-09-11T00:00:00.000Z", + metadata: { + providerStatus: "IN_QUEUE", + queue: { + statusUrl: queue, + resultUrl: queue.replace(/\/status$/, ""), + }, + recoveryHandle: queue, + }, + runId: "run_hidden", + } as never, + { + id: "evt_usage", + eventKey: "usage:receipt", + status: "succeeded", + createdAt: "2026-09-11T00:00:01.000Z", + metadata: { + kind: "billing_usage", + networkFeeUsdMicros: "2982", + feeWei: "1", + }, + }, + ], + } as unknown as RunDetail); + expect(JSON.stringify(clean)).not.toMatch( + /queue\.fal\.run|statusUrl|run_hidden/ + ); + expect(clean.events).toEqual([ + { + id: "evt_progress", + eventKey: "progress:IN_QUEUE:req-1", + status: "running", + createdAt: "2026-09-11T00:00:00.000Z", + metadata: { providerStatus: "IN_QUEUE" }, + }, + { + id: "evt_usage", + eventKey: "usage:receipt", + status: "succeeded", + createdAt: "2026-09-11T00:00:01.000Z", + metadata: { + kind: "billing_usage", + networkFeeUsdMicros: "2982", + feeWei: "1", + }, + }, + ]); +}); it("keeps unavailable asset lineage and billing in public history", () => { const unavailableAt = "2026-09-11T00:00:00Z"; const detail = { diff --git a/tests/contracts/run-http.test.ts b/tests/contracts/run-http.test.ts index 7069578e..c1c1e2e8 100644 --- a/tests/contracts/run-http.test.ts +++ b/tests/contracts/run-http.test.ts @@ -97,6 +97,72 @@ it("fails closed for mismatched canonical identity and invalid filters", async ( ).toBe(400); expect(mocks.list).not.toHaveBeenCalled(); }); +it("strips provider queue URLs from user and admin run detail", async () => { + const queue = "https://queue.fal.run/fal-ai/flux/requests/req-1/status"; + const leaky = { + principalId: owner.principalId, + userId: owner.userId, + externalAccountId: owner.externalAccountId, + id: "run_1", + gatewayRequestId: "job_1", + providerRequestId: null, + provider: "fal", + source: "mcp", + capability: "fal-ai/flux", + modelId: null, + endpoint: null, + status: "running", + submittedArguments: { prompt: "portrait" }, + result: null, + captureVersion: 1, + captureRedactedPaths: [], + errorCode: null, + errorMessage: null, + version: 1, + createdAt: "2026-09-11T00:00:00.000Z", + updatedAt: "2026-09-11T00:00:00.000Z", + startedAt: "2026-09-11T00:00:00.000Z", + completedAt: null, + email: null, + billing: null, + assets: [], + events: [ + { + id: "evt_progress", + eventKey: `progress:IN_QUEUE:req-1:${queue}`, + status: "running", + createdAt: "2026-09-11T00:00:00.000Z", + metadata: { + providerStatus: "IN_QUEUE", + queue: { statusUrl: queue }, + }, + }, + ], + }; + mocks.detail.mockResolvedValue(leaky); + const user = await detail(new Request("https://console.invalid"), { + params: Promise.resolve({ id: "run_1" }), + }); + const actor = { userId: "admin", adminGrantId: "grant", signupId: "signup" }; + mocks.admin.mockResolvedValue(actor); + mocks.adminDetail.mockResolvedValue(leaky); + const admin = await adminDetail(new Request("https://console.invalid"), { + params: Promise.resolve({ id: "run_1" }), + }); + expect(user.status).toBe(200); + expect(admin.status).toBe(200); + for (const response of [user, admin]) { + const body = await response.json(); + expect(JSON.stringify(body)).not.toContain("queue.fal.run"); + expect(body.events[0]).toEqual({ + id: "evt_progress", + eventKey: "progress:IN_QUEUE:req-1", + status: "running", + createdAt: "2026-09-11T00:00:00.000Z", + metadata: { providerStatus: "IN_QUEUE" }, + }); + } +}); it("does not expose foreign or missing runs and masks driver failures", async () => { mocks.detail.mockResolvedValue(null); expect( From e04cde5d67201a36aef5131aa71072b4bc22065f Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Fri, 11 Sep 2026 13:21:18 -0400 Subject: [PATCH 21/26] feat: add autoComplete="off" to buttons and checkboxes for improved UX - Added `autoComplete="off"` attribute to buttons and checkboxes in AccessManager and SelectionCheckbox components to prevent browsers from restoring disabled states across reloads. - Updated tests to verify the presence of the `autoComplete` attribute on relevant elements. --- components/admin/AccessManager.tsx | 2 + components/admin/SelectionCheckbox.tsx | 4 +- lib/assets/public.ts | 48 ++++++++++--------- tests/contracts/admin-access-table.test.tsx | 8 ++++ tests/contracts/public-media.test.ts | 51 +++++++++++++++++++-- 5 files changed, 86 insertions(+), 27 deletions(-) diff --git a/components/admin/AccessManager.tsx b/components/admin/AccessManager.tsx index 84bd6e5e..12d724f8 100644 --- a/components/admin/AccessManager.tsx +++ b/components/admin/AccessManager.tsx @@ -531,6 +531,7 @@ export default function AccessManager() {