Files
agent/scripts/verify-editor-source.mjs

84 lines
5.6 KiB
JavaScript

import { createInterface } from "node:readline";
import { createRequire } from "node:module";
import assert from "node:assert/strict";
import { validateBaseUrl } from "../packages/harness/dist/index.js";
import { parseHls } from "../packages/harness/dist/media/hls.js";
// Read-only acceptance preflight. No signed URLs, auth tokens or subtitles are logged.
const require = createRequire(new URL("../packages/harness/package.json", import.meta.url));
const { createClient } = require("@supabase/supabase-js");
const dramaId = "06aa396d-175a-48ed-ad76-02786ef4d0a9";
const cmsBase = "https://swave.pencil-stub.com/swave-api/api/v1/cms/playback/";
const reader = createInterface({ input: process.stdin });
console.log("Provide CMS email/password/publishableKey via hidden stdin.");
let stage = "login";
async function boundedText(response) {
assert.equal(response.status, 200);
const stream = response.body.getReader();
const chunks = [];
let length = 0;
try {
for (;;) {
const {done,value} = await stream.read();
if (done) break;
length += value.byteLength;
assert(length <= 2 * 1024 * 1024);
chunks.push(value);
}
} finally { await stream.cancel().catch(() => {}); }
return Buffer.concat(chunks).toString("utf8");
}
try {
const input = JSON.parse((await reader[Symbol.asyncIterator]().next()).value);
reader.close();
const client = createClient("https://lmcysqgfhoxppwuorsmu.supabase.co", input.publishableKey, {
auth:{persistSession:false,autoRefreshToken:false,detectSessionInUrl:false},
global:{fetch:(url,init)=>fetch(url,{...init,redirect:"error",signal:AbortSignal.timeout(20_000)})},
});
const {data:auth,error:authError} = await client.auth.signInWithPassword({email:input.email,password:input.password});
input.email = ""; input.password = "";
assert(!authError && auth.session);
const {data:user,error:userError} = await client.auth.getUser(auth.session.access_token);
assert(!userError && user.user?.app_metadata.role === "admin");
stage = "inventory";
const {data:drama,error:dramaError} = await client.from("drama").select("id,title,language,total_episodes").eq("id",dramaId).single();
assert(!dramaError && drama.id === dramaId && drama.language === "en");
const {data:episodes,error:episodeError} = await client.from("episode").select("id,episode_no,drama_id").eq("drama_id",dramaId).order("episode_no").range(0,999);
assert(!episodeError && episodes.length === drama.total_episodes);
assert(episodes.every((e,i)=>e.drama_id===dramaId && e.episode_no===i+1));
const sampled = [episodes[0],episodes[Math.floor(episodes.length/2)],episodes.at(-1)];
const checks = [];
for (const episode of sampled) {
stage = `episode_${episode.episode_no}_signing`;
const headers = {Authorization:`Bearer ${auth.session.access_token}`};
const signedResponse = await fetch(cmsBase+episode.id,{headers,redirect:"error",signal:AbortSignal.timeout(20_000)});
const signed = JSON.parse(await boundedText(signedResponse));
const url = new URL(signed.url);
assert(url.protocol==="https:" && !url.username && !url.password && !url.hash && url.pathname.endsWith(".m3u8"));
await validateBaseUrl(url.origin);
assert(Date.parse(signed.expires_at)>Date.now());
stage = `episode_${episode.episode_no}_manifest`;
const manifest = await boundedText(await fetch(url,{redirect:"error",signal:AbortSignal.timeout(20_000)}));
assert(manifest.trimStart().startsWith("#EXTM3U"));
console.log(JSON.stringify({episodeNo:episode.episode_no,cdnOrigin:url.origin,master:manifest.includes("#EXT-X-STREAM-INF"),segments:(manifest.match(/#EXTINF:/g)??[]).length,encrypted:manifest.includes("#EXT-X-KEY"),byteranges:manifest.includes("#EXT-X-BYTERANGE"),fragmentedMp4:manifest.includes("#EXT-X-MAP")}));
const parsed = parseHls(manifest);
if (parsed.kind === "master") {
const child = new URL(parsed.variants[0], url);
console.log(JSON.stringify({sameOrigin:child.origin===url.origin,childSameQuery:!child.search||child.search===url.search,childWithinEpisode:child.pathname.startsWith(url.pathname.slice(0,url.pathname.lastIndexOf("/")+1)),childExtension:child.pathname.split(".").at(-1)}));
assert(child.origin === url.origin && child.pathname.startsWith(url.pathname.slice(0,url.pathname.lastIndexOf("/")+1)) && !child.username && !child.password && !child.hash);
console.log(JSON.stringify({queryKeys:[...child.searchParams.keys()],authShape:/^\d+-[a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-fA-F0-9]+$/.test(child.searchParams.get("auth_key")??"")}));
const resolved = child;
const variant = await boundedText(await fetch(resolved,{redirect:"error",signal:AbortSignal.timeout(20_000)}));
console.log(JSON.stringify({tags:[...new Set(variant.split("\n").filter(line=>line.startsWith("#")).map(line=>line.split(":")[0]))],segments:(variant.match(/#EXTINF:/g)??[]).length,ends:variant.includes("#EXT-X-ENDLIST"),parseAccepted:(()=>{try{parseHls(variant);return true;}catch{return false;}})()}));
}
stage = `episode_${episode.episode_no}_subtitle`;
const subtitle = await boundedText(await fetch(cmsBase+episode.id+"/subtitles/en.vtt",{headers,redirect:"error",signal:AbortSignal.timeout(20_000)}));
assert(subtitle.trimStart().startsWith("WEBVTT") && subtitle.includes("-->"));
checks.push({episodeNo:episode.episode_no,manifestStatus:200,hlsHeader:true,englishWebVtt:true});
}
console.log(JSON.stringify({result:"PASS",scope:"CMS source preflight only",drama,checks,checkedAt:new Date().toISOString(),mediaDownloaded:false,modelContentSent:false,videoAnalysisComplete:false}));
} catch {
console.error(JSON.stringify({result:"FAIL",stage}));
process.exitCode = 1;
} finally { reader.close(); }