#!/usr/bin/env node
/*
 * sabr-replay.mjs — replays the web player's EXACT SABR request from node.
 *
 * Companion to harvest-pot-console.js. The harvester emits a JSON capture
 * {url, bodyB64, pot, visitor} of the page's own videoplayback POST. This
 * script POSTs that body verbatim to that URL and parses the UMP response,
 * reporting whether MEDIA parts (type 21) actually return bytes — i.e. does a
 * harvested web SABR request pull media when replayed outside the page?
 *
 *   ✓ media bytes  → the request is replayable: a harvested pot+body works,
 *                    the add-on could drive SABR with harvested credentials.
 *   ✗ no media / 403 → the request is bound to the live page session
 *                    (cookies/TLS/single-use) and cannot be replayed as-is.
 *
 * Usage:
 *   1. paste tools/harvest-pot-console.js in DevTools, play the video
 *   2. save the printed JSON to tools/captured.json
 *   3. node tools/sabr-replay.mjs [tools/captured.json]
 */

import { readFile } from 'node:fs/promises';

const path = process.argv[2] || new URL('./captured.json', import.meta.url).pathname;
const cap = JSON.parse(await readFile(path, 'utf8'));
const body = Uint8Array.from(atob(cap.bodyB64), c => c.charCodeAt(0));

// UMP framing: repeated [ varint partType, varint partSize, partSize bytes ]
const readVarint = (buf, pos) => { let r = 0, s = 0, b; do { b = buf[pos++]; r += (b & 0x7f) * Math.pow(2, s); s += 7; } while (b & 0x80); return [r, pos]; };
const PART_NAME = { 20: 'MEDIA_HEADER', 21: 'MEDIA', 22: 'MEDIA_END', 31: 'LIVE_METADATA', 35: 'STREAM_PROTECTION_STATUS', 42: 'SABR_ERROR', 43: 'SABR_REDIRECT', 44: 'SABR_SEEK', 45: 'SABR_CONTEXT_UPDATE', 58: 'NEXT_REQUEST_POLICY', 60: 'FORMAT_INITIALIZATION_METADATA' };

function parseUmp(buf) {
  const hist = {}, bytesByType = {}; let mediaBytes = 0, redirectUrl = null, protection = null;
  let pos = 0;
  while (pos < buf.length) {
    let type, size; [type, pos] = readVarint(buf, pos); if (pos > buf.length) break;
    [size, pos] = readVarint(buf, pos);
    const data = buf.subarray(pos, pos + size); pos += size;
    hist[type] = (hist[type] || 0) + 1;
    bytesByType[type] = (bytesByType[type] || 0) + data.length;
    if (type === 21) mediaBytes += data.length;           // MEDIA payload (incl. small header prefix)
    if (type === 35) { const [st] = readVarint(data, (data[0] >> 3) === 1 && (data[0] & 7) === 0 ? 1 : 0); protection = st; } // field 1 = status
    if (type === 43) {                                     // SABR_REDIRECT: field 1 = url (string)
      let p = 0; const tag = data[0]; if ((tag >> 3) === 1 && (tag & 7) === 2) { let len; [len, p] = readVarint(data, 1); redirectUrl = new TextDecoder().decode(data.subarray(p, p + len)); }
    }
  }
  return { hist, bytesByType, mediaBytes, redirectUrl, protection };
}

async function post(url) {
  const resp = await fetch(url, {
    method: 'POST',
    headers: { 'Referer': 'https://www.youtube.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', 'Origin': 'https://www.youtube.com' },
    body
  });
  const buf = new Uint8Array(await resp.arrayBuffer());
  return { status: resp.status, ctype: resp.headers.get('content-type') || '', buf };
}

console.log(`\n=== sabr-replay  body=${body.length}B  pot=${cap.pot ? cap.pot.slice(0, 12) + '…' : '(none)'} ===`);
console.log(`url: ${cap.url.slice(0, 110)}…`);

let url = cap.url, hop = 0;
for (;;) {
  const r = await post(url);
  console.log(`\nPOST → HTTP ${r.status}  ${r.ctype}  ${r.buf.length}B`);
  if (r.status !== 200) { console.log(`body: ${new TextDecoder().decode(r.buf).slice(0, 300)}`); break; }
  const u = parseUmp(r.buf);
  const parts = Object.entries(u.hist).map(([t, n]) => `${PART_NAME[t] || ('#' + t)}×${n}[${u.bytesByType[t]}B]`).join(', ');
  console.log(`UMP parts: ${parts}`);
  const SP = { 1: 'OK', 2: 'ATTESTATION_PENDING', 3: 'ATTESTATION_REQUIRED' };
  console.log(`MEDIA bytes: ${u.mediaBytes}${u.protection != null ? `   streamProtectionStatus=${u.protection} (${SP[u.protection] || '?'})` : ''}`);
  if (u.redirectUrl && hop < 2) { console.log(`→ SABR_REDIRECT, following (hop ${++hop})`); url = u.redirectUrl; continue; }
  console.log(`\n=== VERDICT: ${u.mediaBytes > 1000 ? '✓ MEDIA FLOWS — harvested web SABR request is replayable' : '✗ NO MEDIA — request bound to live page session / attestation not satisfied'} ===\n`);
  break;
}
