Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Meditor/app/channels/actions.ts | 13++++++++++++-
Meditor/app/storage/actions.ts | 19+++++++++++++++++++
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),