Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
type: fixed
date: 2026-10-23
---

Video playback respects Clip availability.
64 changes: 64 additions & 0 deletions templates/clips/server/routes/api/video/[recordingId].get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,70 @@ describe("/api/video/:recordingId route", () => {
expect(fetch).not.toHaveBeenCalled();
});

it.each([null, "viewer", "editor", "admin", "owner"])(
"does not serve footage from a taken-down Clip to %s",
async (role) => {
const clip = {
visibility: "public",
password: null,
failureCode: "taken_down",
};
mockGetDb.mockReturnValue(
createDbWithSelectSequence(
[{ recordingId: "clip-1" }],
...(role ? [] : [[clip]]),
[footageRow],
),
);
mockResolveAccess.mockResolvedValue(
role ? { role, resource: clip } : null,
);
vi.mocked(fetch).mockResolvedValue(
new Response("media", {
headers: { "content-type": "video/mp4" },
}),
);

const event = makeEvent();
const result = await handler(
event as unknown as Parameters<typeof handler>[0],
);

expect(event.status).toBe(404);
expect(result).toEqual({ error: "Not found" });
expect(mockReadAppState).not.toHaveBeenCalled();
expect(mockFetchS3ObjectByUrl).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
},
);

it.each(["viewer", "owner"])(
"does not serve taken-down footage under a healthy Clip to a %s",
async (role) => {
mockGetDb.mockReturnValue(
createDbWithSelectSequence(
[{ recordingId: "clip-1" }],
[{ ...footageRow, failureCode: "taken_down" }],
),
);
mockResolveAccess.mockResolvedValue({
role,
resource: { visibility: "public", password: null },
});

const event = makeEvent();
const result = await handler(
event as unknown as Parameters<typeof handler>[0],
);

expect(event.status).toBe(404);
expect(result).toEqual({ error: "Not found" });
expect(mockReadAppState).not.toHaveBeenCalled();
expect(mockFetchS3ObjectByUrl).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
},
);

it("requires the clip's password, not the footage's", async () => {
mockGetDb.mockReturnValue(
createDbWithSelectSequence([{ recordingId: "clip-1" }], [footageRow]),
Expand Down
5 changes: 4 additions & 1 deletion templates/clips/server/routes/api/video/[recordingId].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,10 @@ export default defineEventHandler(async (event: H3Event) => {
rec = footage as RecordingRow;
}

if (rec.failureCode === "taken_down") {
if (
gate.failureCode === "taken_down" ||
rec.failureCode === "taken_down"
) {
setResponseStatus(event, 404);
return { error: "Not found" };
}
Expand Down
Loading