#!/usr/bin/env python3 """Minimal HTTP service: POST /api/testflight-invite → ASC external TestFlight enroll. Bind 127.0.0.1 only; Caddy reverse_proxies https://mcp.verae.ai/api/testflight-invite """ from __future__ import annotations import json import os import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer # Allow running from installed dir sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from enroll_lib import enroll, validate_email # noqa: E402 HOST = os.environ.get("ENROLL_BIND", "127.0.0.1") PORT = int(os.environ.get("ENROLL_PORT", "8011")) class Handler(BaseHTTPRequestHandler): server_version = "verae-testflight-enroll/1.0" def log_message(self, fmt, *args): # Avoid logging emails/bodies sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args)) def _cors(self): origin = self.headers.get("Origin") or "" allowed = { "https://mcp.verae.ai", "https://mcp.georgelambert.org", "http://127.0.0.1:8000", "http://localhost:8000", } if origin in allowed: self.send_header("Access-Control-Allow-Origin", origin) self.send_header("Vary", "Origin") self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") def _json(self, code: int, obj: dict): raw = json.dumps(obj).encode("utf-8") self.send_response(code) self.send_header("Content-Type", "application/json; charset=utf-8") self._cors() self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw) def do_OPTIONS(self): self.send_response(204) self._cors() self.end_headers() def do_GET(self): if self.path.split("?")[0] in ("/healthz", "/api/testflight-invite/healthz"): self._json(200, {"ok": True, "service": "verae-testflight-enroll"}) return self._json(404, {"ok": False, "error": "not_found"}) def do_POST(self): path = self.path.split("?")[0] if path not in ("/api/testflight-invite", "/"): self._json(404, {"ok": False, "error": "not_found"}) return length = int(self.headers.get("Content-Length") or "0") if length > 8192: self._json(413, {"ok": False, "error": "too_large", "message": "Request too large."}) return raw = self.rfile.read(length) if length else b"{}" try: body = json.loads(raw.decode("utf-8") or "{}") except json.JSONDecodeError: self._json(400, {"ok": False, "error": "invalid_json", "message": "Expected JSON body."}) return email = body.get("email") or body.get("apple_id") or "" note = body.get("note") or "" try: validate_email(email) except ValueError: self._json(400, { "ok": False, "error": "invalid_email", "message": "Enter a valid Apple ID email address.", }) return try: result = enroll(email, note=str(note)[:500]) except RuntimeError as e: code = str(e) self._json(503, { "ok": False, "error": code, "message": "Enrollment service is not configured. Email marchon@gmail.com with your Apple ID.", }) return except Exception: self._json(500, { "ok": False, "error": "internal", "message": "Enrollment failed unexpectedly. Email marchon@gmail.com with your Apple ID.", }) return self._json(200 if result.get("ok") else 502, result) def main(): httpd = ThreadingHTTPServer((HOST, PORT), Handler) print(f"verae-testflight-enroll listening on {HOST}:{PORT}", flush=True) httpd.serve_forever() if __name__ == "__main__": main()