fix: audio asset playback and orphan-cleanup thumbnail deletion

The audio proxy resolved ownership only through voice comments, so
R2_AUDIO video assets (which store the same proxy path in
videoAsset.sourceUrl) always got 403s. Resolve ownership the way the
image proxy does: query comments and video assets, merge into a
unique-owning-video map, deny on ambiguity.

r2-orphan-cleanup marked videoAsset.sourceUrl as referenced but not
videoAsset.thumbnailUrl, so every R2_VIDEO asset thumbnail older than
the TTL was deleted as an orphan. Widen the query to both columns.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
eehkay
2026-07-19 13:17:13 -07:00
co-authored by Claude Fable 5
parent 33008d33ad
commit 16efba4e19
2 changed files with 37 additions and 22 deletions
+32 -20
View File
@@ -39,38 +39,50 @@ export async function GET(
// Parallelize the DB lookup and session check to narrow the timing delta
// between "asset not found" and "asset found, access denied" responses.
const voiceUrl = `/api/upload/audio/${filename}`;
const [comment, session] = await Promise.all([
db.comment.findFirst({
const projectSelect = {
id: true,
ownerId: true,
workspaceId: true,
visibility: true,
} as const;
const videoSelect = {
id: true,
projectId: true,
project: { select: projectSelect },
} as const;
const [comments, videoAssets, session] = await Promise.all([
db.comment.findMany({
where: { voiceUrl },
take: 2,
select: {
version: {
select: {
video: {
select: {
id: true,
projectId: true,
project: {
select: {
id: true,
ownerId: true,
workspaceId: true,
visibility: true,
},
},
},
},
},
select: { video: { select: videoSelect } },
},
},
}),
db.videoAsset.findMany({
where: { sourceUrl: voiceUrl },
take: 2,
select: { video: { select: videoSelect } },
}),
auth(),
]);
if (!comment) {
const uniqueVideos = new Map<string, (typeof videoAssets)[number]['video']>();
comments.forEach((comment) => {
if (comment.version?.video) uniqueVideos.set(comment.version.video.id, comment.version.video);
});
videoAssets.forEach((videoAsset) => uniqueVideos.set(videoAsset.video.id, videoAsset.video));
if (uniqueVideos.size > 1) {
return apiErrors.forbidden('Access denied');
}
const video = uniqueVideos.values().next().value ?? null;
if (!video) {
return apiErrors.forbidden('Access denied');
}
const { video } = comment.version;
const access = await checkProjectAccess(video.project, session?.user?.id);
if (!access.hasAccess) {