fix(media): load inbound media store URIs

This commit is contained in:
Peter Steinberger
2026-04-22 20:23:19 +01:00
parent 0e761cdba8
commit e5b67b7ebd
2 changed files with 64 additions and 1 deletions

View File

@@ -431,4 +431,23 @@ describe("loadWebMedia", () => {
code: "path-not-allowed",
});
});
it("hydrates inbound media store URIs before allowed-root checks", async () => {
const id = `signal-${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
const filePath = path.join(stateDir, "media", "inbound", id);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, Buffer.from(TINY_PNG_BASE64, "base64"));
try {
const result = await loadWebMedia(`media://inbound/${id}`, {
maxBytes: 1024 * 1024,
});
expect(result.kind).toBe("image");
expect(result.buffer.length).toBeGreaterThan(0);
expect(result.fileName).toBe(id);
} finally {
await fs.rm(filePath, { force: true });
}
});
});

View File

@@ -27,6 +27,7 @@ import {
mimeTypeFromFilePath,
normalizeMimeType,
} from "./mime.js";
import { resolveMediaBufferPath } from "./store.js";
export { getDefaultLocalRoots, LocalMediaAccessError };
export type { LocalMediaAccessErrorCode };
@@ -56,6 +57,46 @@ type WebMediaOptions = {
hostReadCapability?: boolean;
};
async function resolveMediaStoreUriToPath(mediaUrl: string): Promise<string | null> {
if (!mediaUrl.startsWith("media://")) {
return null;
}
let parsed: URL;
try {
parsed = new URL(mediaUrl);
} catch (err) {
throw new LocalMediaAccessError("invalid-path", `Invalid media URI: ${mediaUrl}`, {
cause: err,
});
}
if (parsed.hostname !== "inbound") {
throw new LocalMediaAccessError(
"path-not-allowed",
`Unsupported media URI location: ${parsed.hostname || "(missing)"}`,
);
}
let id: string;
try {
id = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
} catch (err) {
throw new LocalMediaAccessError("invalid-path", `Invalid media URI: ${mediaUrl}`, {
cause: err,
});
}
if (!id || id.includes("/")) {
throw new LocalMediaAccessError("invalid-path", `Invalid media URI: ${mediaUrl}`);
}
try {
return await resolveMediaBufferPath(id, "inbound");
} catch (err) {
throw new LocalMediaAccessError(
"invalid-path",
err instanceof Error ? err.message : `Invalid media URI: ${mediaUrl}`,
{ cause: err },
);
}
}
function resolveWebMediaOptions(params: {
maxBytesOrOptions?: number | WebMediaOptions;
options?: { ssrfPolicy?: SsrFPolicy; localRoots?: readonly string[] | "any" };
@@ -356,7 +397,10 @@ async function loadWebMediaInternal(
} = options;
// Strip MEDIA: prefix used by agent tools (e.g. TTS) to tag media paths.
// Be lenient: LLM output may add extra whitespace (e.g. " MEDIA : /tmp/x.png").
mediaUrl = mediaUrl.replace(/^\s*MEDIA\s*:\s*/i, "");
if (!/^\s*media:\/\//i.test(mediaUrl)) {
mediaUrl = mediaUrl.replace(/^\s*MEDIA\s*:\s*/i, "");
}
mediaUrl = (await resolveMediaStoreUriToPath(mediaUrl)) ?? mediaUrl;
// Use fileURLToPath for proper handling of file:// URLs (handles file://localhost/path, etc.)
if (mediaUrl.startsWith("file://")) {
try {