#!/usr/bin/env python3 """Build a fail-closed public demo video URL check for Devpost submission.""" from __future__ import annotations import argparse import json import os from datetime import datetime, timezone from pathlib import Path from typing import Any, Callable from urllib.error import HTTPError, URLError from urllib.parse import parse_qsl, urlparse from urllib.request import Request, urlopen ROOT = Path(__file__).resolve().parents[1] SCHEMA = "proofframe.public_video_check.v1" DEFAULT_JSON = ROOT / "docs" / "assets" / "public-video-check.json" DEFAULT_MD = ROOT / "docs" / "assets" / "public-video-check.md" TIMEOUT_SECONDS = 20 TOKEN_PARAM_HINTS = { "access_key", "access_token", "auth", "authorization", "expires", "key", "signature", "sig", "signed", "token", } PRIVATE_HOST_PREFIXES = ("localhost", "127.", "0.", "10.", "172.16.", "172.17.", "172.18.", "172.19.", "192.168.") OFFICIAL_VIDEO_HOSTS = { "youtube": ("youtube.com", "youtu.be", "youtube-nocookie.com"), "vimeo": ("vimeo.com",), "youku": ("youku.com",), } FetchResult = dict[str, Any] Fetcher = Callable[[str, int], FetchResult] def utc_now() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") def load_json(path: Path) -> dict[str, Any]: try: return json.loads(path.read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return {} def has_placeholder(value: str) -> bool: lowered = value.strip().lower() return not lowered or lowered.startswith("tbd") or "tbd after" in lowered or lowered in {"https://...", "http://..."} def candidate_video_url(root: Path, explicit_url: str | None = None) -> str: if explicit_url: return explicit_url.strip() env_url = os.environ.get("PROOFFRAME_PUBLIC_VIDEO_URL", "").strip() if env_url: return env_url packet = load_json(root / "docs" / "assets" / "devpost-submission-packet.json") return str(packet.get("video_url") or "").strip() def event_requirements(root: Path) -> dict[str, Any]: snapshot = load_json(root / "docs" / "assets" / "devpost-event-snapshot.json") requirements = ((snapshot.get("rules") or {}).get("requirements") or {}) validation = snapshot.get("validation") or {} return { "path": "docs/assets/devpost-event-snapshot.json", "schema_ok": snapshot.get("schema") == "proofframe.devpost_event_snapshot.v1", "validation_ok": validation.get("ok") is True, "demo_video": requirements.get("demo_video") is True, "video_under_three_minutes": requirements.get("video_under_three_minutes") is True, "public_video_host": requirements.get("public_video_host") is True, } def storyboard_requirements(root: Path) -> dict[str, Any]: storyboard = load_json(root / "docs" / "assets" / "demo-storyboard.json") return { "path": "docs/assets/demo-storyboard.json", "schema_ok": storyboard.get("schema") == "proofframe.demo_storyboard.v1", "under_time_limit": storyboard.get("under_time_limit") is True, "total_seconds": storyboard.get("total_seconds"), "max_seconds": storyboard.get("max_seconds"), "public_video_ready": storyboard.get("public_video_ready"), } def official_video_host_family(host: str) -> str | None: for family, domains in OFFICIAL_VIDEO_HOSTS.items(): for domain in domains: if host == domain or host.endswith(f".{domain}"): return family return None def video_url_analysis(url: str) -> dict[str, Any]: if has_placeholder(url): return { "url": url, "present": False, "scheme_ok": False, "host": None, "host_public": False, "official_host": False, "official_host_family": None, "has_credentials": False, "token_params": [], "safe_query": False, "ok": False, "reason": "missing_or_placeholder", } parsed = urlparse(url) query_keys = {key.lower() for key, _ in parse_qsl(parsed.query, keep_blank_values=True)} token_params = sorted(key for key in query_keys if key in TOKEN_PARAM_HINTS or "token" in key or "sig" in key) host = (parsed.hostname or "").lower() host_public = bool(host) and not host.startswith(PRIVATE_HOST_PREFIXES) official_host_family = official_video_host_family(host) official_host = official_host_family is not None has_credentials = bool(parsed.username or parsed.password) scheme_ok = parsed.scheme in {"http", "https"} safe_query = not token_params ok = bool(scheme_ok and host_public and official_host and not has_credentials and safe_query) if ok: reason = "ok" elif scheme_ok and host_public and safe_query and not has_credentials and not official_host: reason = "unsupported_video_host" else: reason = "unsafe_or_private_url" return { "url": url, "present": True, "scheme_ok": scheme_ok, "host": host, "host_public": host_public, "official_host": official_host, "official_host_family": official_host_family, "has_credentials": has_credentials, "token_params": token_params, "safe_query": safe_query, "ok": ok, "reason": reason, } def fetch_url(url: str, timeout: int = TIMEOUT_SECONDS) -> FetchResult: request = Request( url, headers={ "User-Agent": "ProofFrame public video verifier", "Range": "bytes=0-2048", }, ) try: with urlopen(request, timeout=timeout) as response: response.read(2048) return { "checked": True, "ok": 200 <= int(response.status) < 400, "status": response.status, "content_type": response.headers.get("content-type"), "final_url": response.geturl(), "error": None, } except HTTPError as error: return { "checked": True, "ok": 200 <= int(error.code) < 400, "status": error.code, "content_type": error.headers.get("content-type") if error.headers else None, "final_url": url, "error": str(error), } except (URLError, TimeoutError) as error: return { "checked": True, "ok": False, "status": None, "content_type": None, "final_url": url, "error": str(error), } def unchecked_url(url: str) -> FetchResult: return { "checked": False, "ok": False, "status": None, "content_type": None, "final_url": url, "error": "URL verification was not requested.", } def check_item(check_id: str, ok: bool, detail: str, evidence: str) -> dict[str, Any]: return {"id": check_id, "ok": ok, "detail": detail, "evidence": evidence} def build_checks( *, event: dict[str, Any], storyboard: dict[str, Any], url: dict[str, Any], access: FetchResult, ) -> list[dict[str, Any]]: return [ check_item( "official_event_video_requirements", bool(event["schema_ok"] and event["validation_ok"] and event["demo_video"] and event["public_video_host"]), ( f"demo_video={event['demo_video']}; public_video_host={event['public_video_host']}; " f"snapshot validation={event['validation_ok']}." ), event["path"], ), check_item( "storyboard_under_three_minutes", bool(storyboard["schema_ok"] and storyboard["under_time_limit"] and event["video_under_three_minutes"]), f"Storyboard duration is {storyboard['total_seconds']}s / {storyboard['max_seconds']}s.", storyboard["path"], ), check_item( "video_url_present", bool(url["present"]), f"Video URL source is {'present' if url['present'] else 'missing or placeholder'}.", "docs/assets/devpost-submission-packet.json or PROOFFRAME_PUBLIC_VIDEO_URL", ), check_item( "video_url_public_and_safe", bool(url["scheme_ok"] and url["host_public"] and not url["has_credentials"] and url["safe_query"]), ( f"scheme_ok={url['scheme_ok']}; host={url['host']}; " f"host_public={url['host_public']}; token_params={url['token_params']}." ), "public video URL", ), check_item( "video_url_official_public_host", bool(url["official_host"]), ( f"host={url['host']}; official_host_family={url['official_host_family']}; " "allowed families are YouTube, Vimeo, and Youku." ), "Devpost public video host requirement", ), check_item( "video_url_accessible", bool(access["checked"] and access["ok"]), f"checked={access['checked']}; status={access['status']}; error={access['error']}.", "public video URL", ), ] def build_report( root: Path = ROOT, *, video_url: str | None = None, verify_url: bool = False, fetcher: Fetcher = fetch_url, ) -> dict[str, Any]: root = root.resolve() selected_url = candidate_video_url(root, video_url) event = event_requirements(root) storyboard = storyboard_requirements(root) url = video_url_analysis(selected_url) access = fetcher(selected_url, TIMEOUT_SECONDS) if verify_url and url["ok"] else unchecked_url(selected_url) checks = build_checks(event=event, storyboard=storyboard, url=url, access=access) safe_to_submit = all(item["ok"] for item in checks) if safe_to_submit: mode = "public_video_verified" elif not url["present"]: mode = "pending_video_url" elif not url["ok"]: mode = "unsafe_video_url" else: mode = "needs_url_verification" return { "schema": SCHEMA, "created_at": utc_now(), "mode": mode, "ok": safe_to_submit, "safe_to_submit": safe_to_submit, "video_url": selected_url, "event_requirements": event, "storyboard_requirements": storyboard, "url_analysis": url, "access_check": access, "checks": checks, "next_actions": build_next_actions(mode, checks), } def build_next_actions(mode: str, checks: list[dict[str, Any]]) -> list[str]: failed = {item["id"] for item in checks if not item["ok"]} actions: list[str] = [] if mode == "pending_video_url": actions.append("Record and upload the final demo video, then set PROOFFRAME_PUBLIC_VIDEO_URL.") if "storyboard_under_three_minutes" in failed: actions.append("Keep the final demo video under the event's 3-minute limit.") if "video_url_public_and_safe" in failed: actions.append("Use a public http(s) video URL without credential, token, signature, or expiry query parameters.") if "video_url_official_public_host" in failed: actions.append("Upload the final demo video to YouTube, Vimeo, or Youku before strict final submission.") if "video_url_accessible" in failed: actions.append("Run python scripts/public_video_check.py --video-url \"$PROOFFRAME_PUBLIC_VIDEO_URL\" --verify-url --strict-final after upload.") if "official_event_video_requirements" in failed: actions.append("Refresh docs/assets/devpost-event-snapshot.json from Devpost before final submission.") return list(dict.fromkeys(actions))[:6] def render_markdown(report: dict[str, Any]) -> str: lines = [ "# ProofFrame Public Video Check", "", f"Mode: `{report['mode']}`", f"OK: `{str(report['ok']).lower()}`", f"Safe to submit: `{str(report['safe_to_submit']).lower()}`", f"Video URL: `{report['video_url'] or 'missing'}`", "", "## Checks", "", ] for item in report["checks"]: marker = "OK" if item["ok"] else "BLOCKED" lines.append(f"- {marker} `{item['id']}`: {item['detail']} Evidence: `{item['evidence']}`") access = report["access_check"] lines.extend( [ "", "## Access Check", "", f"- Checked: `{str(access.get('checked')).lower()}`", f"- Status: `{access.get('status')}`", f"- Content type: `{access.get('content_type')}`", f"- Final URL: `{access.get('final_url')}`", ] ) lines.extend(["", "## Next Actions", ""]) if report["next_actions"]: lines.extend(f"- {action}" for action in report["next_actions"]) else: lines.append("- Public video URL is ready for final Devpost copy.") lines.append("") lines.append("This report stores only a public video URL and accessibility metadata, never credentials or browser state.") return "\n".join(lines).rstrip() + "\n" def write_outputs(report: dict[str, Any], json_path: Path, markdown_path: Path) -> None: json_path.parent.mkdir(parents=True, exist_ok=True) markdown_path.parent.mkdir(parents=True, exist_ok=True) json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") markdown_path.write_text(render_markdown(report), encoding="utf-8") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Build a public video URL check for final Devpost submission.") parser.add_argument("--root", type=Path, default=ROOT) parser.add_argument("--video-url") parser.add_argument("--verify-url", action="store_true", help="Perform a GET-only public URL accessibility check.") parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON) parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD) parser.add_argument("--strict-final", action="store_true", help="Fail unless the public video is verified.") return parser def main() -> None: args = build_parser().parse_args() report = build_report(args.root, video_url=args.video_url, verify_url=args.verify_url) write_outputs(report, args.json_out, args.markdown_out) print( json.dumps( { "ok": report["ok"], "mode": report["mode"], "json": str(args.json_out), "markdown": str(args.markdown_out), "safe_to_submit": report["safe_to_submit"], "next_actions": report["next_actions"], }, indent=2, ) ) if args.strict_final and not report["safe_to_submit"]: raise SystemExit(2) if __name__ == "__main__": main()