backblaze-proofframe / scripts /devpost_submission_receipt.py
junhaocheng
Sync post-credential gate hardening
f54fafa
Raw
History Blame Contribute Delete
9.21 kB
#!/usr/bin/env python3
"""Create a public-safe Devpost final submission receipt."""
from __future__ import annotations
import argparse
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import parse_qsl, urlparse
ROOT = Path(__file__).resolve().parents[1]
SCHEMA = "proofframe.devpost_submission_receipt.v1"
DEFAULT_JSON = ROOT / "docs" / "assets" / "devpost-submission-receipt.json"
DEFAULT_MD = ROOT / "docs" / "assets" / "devpost-submission-receipt.md"
SAFE_QUERY_KEYS = {"ref_content", "ref_feature", "utm_source", "utm_medium", "utm_campaign"}
SECRET_QUERY_RE = re.compile(r"(?i)(token|cookie|key|secret|signature|session|auth|credential)")
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def normalize_text(value: str | None) -> str:
return (value or "").strip()
def is_placeholder(value: str | None) -> bool:
text = normalize_text(value).lower()
return not text or text.startswith(("tbd", "<", "pending", "todo"))
def parse_submitted_at(value: str | None) -> tuple[str | None, str | None]:
if is_placeholder(value):
return None, "Submitted timestamp is missing."
text = normalize_text(value)
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError:
return None, "Submitted timestamp must be ISO-8601, for example 2026-08-03T21:00:00Z."
if parsed.tzinfo is None:
return None, "Submitted timestamp must include a timezone."
return parsed.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), None
def validate_project_url(value: str | None) -> tuple[str | None, list[str]]:
if is_placeholder(value):
return None, ["Public Devpost project URL is missing."]
text = normalize_text(value)
parsed = urlparse(text)
findings: list[str] = []
if parsed.scheme != "https":
findings.append("Devpost project URL must use https.")
if parsed.netloc.lower() != "devpost.com":
findings.append("Devpost project URL must use devpost.com.")
if not parsed.path.startswith("/software/") or len(parsed.path.rstrip("/").split("/")) < 3:
findings.append("Devpost project URL must point to /software/<project-slug>.")
if parsed.username or parsed.password or parsed.fragment:
findings.append("Devpost project URL must not include userinfo or fragments.")
query_keys = {key for key, _ in parse_qsl(parsed.query, keep_blank_values=True)}
unsafe_query = sorted(key for key in query_keys if key not in SAFE_QUERY_KEYS or SECRET_QUERY_RE.search(key))
if unsafe_query:
findings.append("Devpost project URL contains unsafe query parameters: " + ", ".join(unsafe_query))
clean_url = parsed._replace(query="", fragment="").geturl().rstrip("/")
return clean_url, findings
def validate_confirmation(value: str | None) -> tuple[str | None, str | None]:
text = normalize_text(value)
if len(text) < 10:
return None, "Confirmation note should briefly state that Devpost accepted/submitted the project."
if SECRET_QUERY_RE.search(text):
return None, "Confirmation note must not contain token/cookie/key/session-like text."
return text, None
def validate_screenshot(root: Path, value: str | None) -> tuple[str | None, str | None]:
if is_placeholder(value):
return None, None
path = Path(normalize_text(value))
if path.is_absolute():
try:
relative = path.resolve().relative_to(root.resolve())
except ValueError:
return None, "Screenshot path must stay inside the project."
else:
relative = path
if not str(relative).startswith("docs/assets/"):
return None, "Screenshot path must be under docs/assets/."
if relative.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp"}:
return None, "Screenshot must be a png, jpg, jpeg, or webp file."
if not (root / relative).exists():
return None, "Screenshot path does not exist."
return str(relative), None
def build_receipt(
*,
root: Path = ROOT,
project_url: str | None = None,
submitted_at: str | None = None,
confirmation_note: str | None = None,
screenshot: str | None = None,
) -> dict[str, Any]:
root = root.resolve()
clean_url, url_findings = validate_project_url(project_url)
clean_submitted_at, submitted_error = parse_submitted_at(submitted_at)
clean_confirmation, confirmation_error = validate_confirmation(confirmation_note)
clean_screenshot, screenshot_error = validate_screenshot(root, screenshot)
findings: list[dict[str, str]] = []
findings.extend({"field": "project_url", "detail": detail} for detail in url_findings)
if submitted_error:
findings.append({"field": "submitted_at", "detail": submitted_error})
if confirmation_error:
findings.append({"field": "confirmation_note", "detail": confirmation_error})
if screenshot_error:
findings.append({"field": "screenshot", "detail": screenshot_error})
ok = not findings
return {
"schema": SCHEMA,
"created_at": utc_now(),
"mode": "submitted" if ok else "pending_submission",
"ok": ok,
"project": "ProofFrame",
"event": "Backblaze Generative Media Hackathon",
"project_url": clean_url,
"submitted_at": clean_submitted_at,
"confirmation_note": clean_confirmation,
"screenshot": clean_screenshot,
"findings": findings,
"secret_policy": "Receipt stores only public Devpost URL, timestamp, and confirmation text; no cookies, browser sessions, tokens, or private form data.",
"next_actions": [
"Submit the project in Devpost after strict final gates are green.",
'Run python scripts/devpost_submission_receipt.py --project-url "$PROOFFRAME_DEVPOST_PROJECT_URL" --submitted-at "$PROOFFRAME_DEVPOST_SUBMITTED_AT" --confirmation-note "Devpost accepted/submitted the ProofFrame project."',
"Mark T042 done only after this receipt is ok.",
"Rerun python scripts/final_submission_control.py --strict-final.",
],
}
def render_markdown(report: dict[str, Any]) -> str:
lines = [
"# ProofFrame Devpost Submission Receipt",
"",
f"Mode: `{report['mode']}`",
f"OK: `{str(report['ok']).lower()}`",
f"Created: `{report['created_at']}`",
f"Project URL: {report['project_url'] or '`pending`'}",
f"Submitted at: `{report['submitted_at'] or 'pending'}`",
"",
report["secret_policy"],
"",
"## Confirmation",
"",
report["confirmation_note"] or "Pending final Devpost submission.",
"",
"## Findings",
"",
]
if report["findings"]:
lines.extend(f"- `{item['field']}`: {item['detail']}" for item in report["findings"])
else:
lines.append("- None")
lines.extend(["", "## Next Actions", ""])
lines.extend(f"- {action}" for action in report["next_actions"])
return "\n".join(lines) + "\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="Create a public-safe Devpost submission receipt.")
parser.add_argument("--project-url", help="Public Devpost /software/<slug> URL after final submission.")
parser.add_argument("--submitted-at", help="ISO-8601 submitted timestamp, for example 2026-08-03T21:00:00Z.")
parser.add_argument("--confirmation-note", help="Short public-safe confirmation note.")
parser.add_argument("--screenshot", help="Optional public-safe screenshot under docs/assets/.")
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="Exit nonzero unless the receipt is complete.")
return parser
def main() -> None:
args = build_parser().parse_args()
report = build_receipt(
project_url=args.project_url,
submitted_at=args.submitted_at,
confirmation_note=args.confirmation_note,
screenshot=args.screenshot,
)
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),
"project_url": report["project_url"],
"findings": len(report["findings"]),
},
indent=2,
)
)
if args.strict_final and not report["ok"]:
raise SystemExit(2)
if __name__ == "__main__":
main()