commit 9fde71d250691e541784dfde8bf00fc280d2284a
parent 0204f2881f0cfe3688173a55f5bb7c98d50a6bc1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 22 Sep 2026 16:43:12 -0400
editor: two refusals the operator could not read
`evictClipWindowsAction` took `slug` off the wire — `/api/ops/evict-clips` passes
whatever the body said — and `evictChannel` path-joins it under `channelsDir`
before walking and DELETING. Shape is checked first, because that is what
forbids "/" and ".." and it runs before any join; then existence, because a typo
naming no channel should be a refusal an agent can read rather than a silent
zero-byte success over a directory that was never there. In the action, not the
route: the ops layer is adapters and may hold no rule of its own.
`deleteChannelAction` called `deleteChannel` outside try/catch, so its
`.relocating.json` refusal — media in transition is not a channel anyone may
delete — arrived as a thrown server action and a digest-shaped error page, while
the busy refusal two lines above it arrived as a sentence on the form. Both are
refusals; both reach the form now. The `redirect` stays outside the catch: it
throws NEXT_REDIRECT as its control flow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/editor/app/channels/actions.ts b/editor/app/channels/actions.ts
@@ -608,7 +608,18 @@ export async function deleteChannelAction(
// asked about.
const busy = channelMediaBusyReason(slug, "deleting it");
if (busy) return { error: busy };
- await deleteChannel(getPaths(), slug);
+ // THE OTHER REFUSAL REACHES THE FORM THE SAME WAY. `deleteChannel` THROWS
+ // when `.relocating.json` is present — media in transition is not a channel
+ // anyone may delete — and an uncaught throw from a server action is a
+ // digest-shaped error page, not the sentence above it. Both refusals are
+ // refusals; they belong in the same place, on the same form, in the
+ // operator's words. The `redirect` below stays OUTSIDE this: it throws
+ // NEXT_REDIRECT as its control flow and a catch here would swallow it.
+ try {
+ await deleteChannel(getPaths(), slug);
+ } catch (e) {
+ return { error: (e as Error).message };
+ }
// Same reason as createChannelAction: the deleted channel keeps a leaf in
// every compiled tree until something recompiles. A leaf matching nothing is
// harmless to dispatch and confusing to read.
diff --git a/editor/app/storage/actions.ts b/editor/app/storage/actions.ts
@@ -26,6 +26,10 @@ import {
readDirMarker,
} from "yt-dlp-transcript-common/controller/relocateDir";
import { savedVideosMarkerPath } from "yt-dlp-transcript-common/controller/relocateSavedVideos";
+import {
+ channelExists,
+ isValidChannelSlug,
+} from "yt-dlp-transcript-common/controller/channels";
import { channelMediaBusyReason } from "../channels/lib/mediaBusy";
import { enqueueRepointJob } from "./lib/repointJob";
import { enqueueSavedVideosRelocation } from "./lib/savedVideosJob";
@@ -425,6 +429,21 @@ export async function evictClipWindowsAction(opts: {
error: "The age must be a number of days, zero or more.",
};
}
+ // THE SLUG COMES OFF THE WIRE — `/api/ops/evict-clips` passes whatever the
+ // body said — and `evictChannel` path-joins it under `channelsDir` before
+ // walking and DELETING. Shape first, because that is what forbids "/" and
+ // ".." and it runs before any join; then existence, because a typo naming no
+ // channel should be a refusal an agent can read, not a silent zero-byte
+ // "success" over a directory that was never there. A server action is the
+ // guard here: the ops route is an adapter and may hold no rule of its own.
+ if (opts.slug !== undefined) {
+ if (!isValidChannelSlug(opts.slug)) {
+ return { ok: false, error: `"${opts.slug}" is not a valid channel slug` };
+ }
+ if (!(await channelExists(getPaths(), opts.slug))) {
+ return { ok: false, error: `Channel "${opts.slug}" not found` };
+ }
+ }
return enqueueEvictClipWindows({
...(opts.slug ? { slug: opts.slug } : {}),
olderThanDays: Math.floor(opts.olderThanDays),