From 448d349af8fb5f3a60ff714bcf5d7b65cd98629c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 14 Aug 2026 17:02:05 -0700 Subject: [PATCH] Address response bodies by response id instead of a filesystem path The body commands took a path from the client, so a token holder could read any file the process could. They now take a response id and resolve the location themselves, and the UI never sees a path at all: the desktop host asks the backend where the file is, and the bridge fetches /responses/:id/body. Ephemeral responses (GraphQL introspection) never reach the database, so resolution falls back to the path send writes them to. --- .../components/HttpResponsePane.tsx | 22 +++-- .../responseViewers/AudioViewer.tsx | 18 ++-- .../responseViewers/ImageViewer.tsx | 18 ++-- .../components/responseViewers/PdfViewer.tsx | 12 +-- .../responseViewers/VideoViewer.tsx | 18 ++-- apps/yaak-client/hooks/useResponseBodyUrl.ts | 23 +++++ apps/yaak-client/lib/responseBody.ts | 29 ++++-- crates-server/yaak-server/src/rpc/commands.rs | 69 +++++++++++--- crates-server/yaak-server/src/rpc/mod.rs | 5 + .../yaak-app-client/bindings/gen_rpc.ts | 8 +- crates-tauri/yaak-app-client/src/lib.rs | 91 +++++++++++++++---- crates-tauri/yaak-app-client/src/rpc_ext.rs | 22 ++++- crates/yaak/src/lib.rs | 1 + crates/yaak/src/responses.rs | 53 +++++++++++ crates/yaak/src/send.rs | 3 +- packages/platform/src/bridge/index.ts | 41 +++++---- packages/platform/src/tauri/index.ts | 22 ++++- packages/platform/src/types.ts | 21 ++++- 18 files changed, 366 insertions(+), 110 deletions(-) create mode 100644 apps/yaak-client/hooks/useResponseBodyUrl.ts create mode 100644 crates/yaak/src/responses.rs diff --git a/apps/yaak-client/components/HttpResponsePane.tsx b/apps/yaak-client/components/HttpResponsePane.tsx index 645de078..9df20748 100644 --- a/apps/yaak-client/components/HttpResponsePane.tsx +++ b/apps/yaak-client/components/HttpResponsePane.tsx @@ -8,6 +8,7 @@ import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse"; import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents"; import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse"; import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText"; +import { useResponseBodyUrl } from "../hooks/useResponseBodyUrl"; import { useResponseViewMode } from "../hooks/useResponseViewMode"; import { useSaveResponse } from "../hooks/useSaveResponse"; import { useTimelineViewMode } from "../hooks/useTimelineViewMode"; @@ -409,14 +410,13 @@ function EnsureCompleteResponse({ Component, }: { response: HttpResponse; - Component: ComponentType<{ bodyPath: string }>; + Component: ComponentType<{ url: string }>; }) { - if (response.bodyPath === null) { - return
Empty response body
; - } + // Wait until the response has been fully-downloaded before asking for it + const complete = response.state === "closed"; + const bodyUrl = useResponseBodyUrl(complete ? response : null); - // Wait until the response has been fully-downloaded - if (response.state !== "closed") { + if (!complete || bodyUrl.isPending) { return ( @@ -424,7 +424,15 @@ function EnsureCompleteResponse({ ); } - return ; + if (bodyUrl.error) { + return {String(bodyUrl.error)}; + } + + if (bodyUrl.data == null) { + return
Empty response body
; + } + + return ; } function HttpSvgViewer({ response }: { response: HttpResponse }) { diff --git a/apps/yaak-client/components/responseViewers/AudioViewer.tsx b/apps/yaak-client/components/responseViewers/AudioViewer.tsx index 89f102de..880e7649 100644 --- a/apps/yaak-client/components/responseViewers/AudioViewer.tsx +++ b/apps/yaak-client/components/responseViewers/AudioViewer.tsx @@ -1,29 +1,29 @@ import { useEffect, useState } from "react"; -import { platform } from "@yaakapp-internal/platform"; interface Props { - bodyPath?: string; + /** A URL the host resolved, for a body it already stored. */ + url?: string; data?: Uint8Array; mimeType?: string; } -export function AudioViewer({ bodyPath, data, mimeType }: Props) { +export function AudioViewer({ url, data, mimeType }: Props) { const [src, setSrc] = useState(); useEffect(() => { - if (bodyPath) { - setSrc(platform.files.url(bodyPath)); + if (url) { + setSrc(url); } else if (data) { // The type matters here in a way it doesn't for an image: a media element goes by what // the blob declares rather than sniffing it, so an Ogg labelled as MP3 won't play const blob = new Blob([new Uint8Array(data)], { type: mimeType ?? "audio/mpeg" }); - const url = URL.createObjectURL(blob); - setSrc(url); - return () => URL.revokeObjectURL(url); + const objectUrl = URL.createObjectURL(blob); + setSrc(objectUrl); + return () => URL.revokeObjectURL(objectUrl); } else { setSrc(undefined); } - }, [bodyPath, data, mimeType]); + }, [url, data, mimeType]); // oxlint-disable-next-line jsx-a11y/media-has-caption return