-
Notifications
You must be signed in to change notification settings - Fork 0
Gate full audits behind Stripe payment #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,58 +1,147 @@ | ||
| import base64 | ||
| import json | ||
| import os | ||
| import re | ||
| import subprocess | ||
| import tempfile | ||
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | ||
| from pathlib import Path | ||
| from urllib.parse import parse_qs, quote, urlsplit | ||
| from urllib.request import Request, urlopen | ||
|
|
||
| from pubskill_lib.audit import audit_path | ||
|
|
||
| REPO = re.compile(r"^https://github\.com/[^/]+/[^/]+(?:\.git)?$") | ||
| PAYMENT_LINK_ID = "plink_1UFLuMAyiOEDWiRnLUiYEx3Y" | ||
| PAYMENT_LINK_URL = "https://buy.stripe.com/14A6oG5Sk9MWg8z3UO5EY01" | ||
| PRICE_CENTS = 1900 | ||
| FREE_FINDINGS = 3 | ||
|
Comment on lines
+15
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Stripe Payment Link and paid server endpoint turn this distribution repository into a hosted SaaS, while the applicable work order explicitly lists “Do not host a SaaS” in AGENTS.md reference: AGENTS.md:L3-L5 Useful? React with 👍 / 👎. |
||
|
|
||
|
|
||
| def run_audit(repo_url): | ||
| if not REPO.fullmatch(repo_url): | ||
| raise ValueError("public github.com repository required") | ||
|
|
||
| with tempfile.TemporaryDirectory() as tmp: | ||
| target = os.path.join(tmp, "repo") | ||
| subprocess.run( | ||
| ["git", "clone", "--depth=1", "--single-branch", repo_url, target], | ||
| check=True, | ||
| timeout=120, | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL, | ||
| ) | ||
| return audit_path(target) | ||
|
|
||
|
|
||
| def stripe_session(session_id): | ||
| key = os.environ.get("STRIPE_SECRET_KEY") | ||
| if not key: | ||
| raise RuntimeError("payment verification is not configured") | ||
|
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
|
|
||
| auth = base64.b64encode(f"{key}:".encode()).decode() | ||
| request = Request( | ||
| f"https://api.stripe.com/v1/checkout/sessions/{quote(session_id, safe='')}", | ||
| headers={"Authorization": f"Basic {auth}"}, | ||
| ) | ||
| with urlopen(request, timeout=15) as response: | ||
| return json.load(response) | ||
|
|
||
|
|
||
| def paid_repo(session): | ||
| if session.get("payment_status") != "paid": | ||
| raise PermissionError("payment is not complete") | ||
| if session.get("payment_link") != PAYMENT_LINK_ID: | ||
| raise PermissionError("payment does not belong to this product") | ||
| if session.get("currency") != "usd" or session.get("amount_total") != PRICE_CENTS: | ||
| raise PermissionError("payment amount does not match this product") | ||
|
|
||
| for field in session.get("custom_fields") or []: | ||
| if field.get("key") == "githubrepo" and field.get("type") == "text": | ||
| repo_url = (field.get("text") or {}).get("value", "").strip() | ||
| if REPO.fullmatch(repo_url): | ||
| return repo_url | ||
|
Comment on lines
+61
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a buyer mistypes the Checkout custom field, uses a common but rejected form such as a trailing slash, or supplies a private/unavailable repository, Stripe can complete the $19 payment before this validation or the subsequent clone fails. The paid endpoint then returns only an error, with no correction or refund path, so a successfully charged customer receives no audit; create the Checkout Session from the already validated preview URL or provide a secure recovery flow. Useful? React with 👍 / 👎.
Comment on lines
+61
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a buyer makes a typo in the Payment Link's free-text Useful? React with 👍 / 👎. |
||
|
|
||
| raise ValueError("paid checkout is missing a valid GitHub repository URL") | ||
|
|
||
|
|
||
| class Handler(BaseHTTPRequestHandler): | ||
| def reply(self, status, body): | ||
| data = json.dumps(body).encode() | ||
| self.send_response(status) | ||
| self.send_header("Content-Type", "application/json") | ||
| self.send_header("Cache-Control", "no-store") | ||
| self.send_header("Content-Length", str(len(data))) | ||
| self.end_headers() | ||
| self.wfile.write(data) | ||
|
|
||
| def do_GET(self): | ||
| def page(self): | ||
| data = Path("index.html").read_bytes() | ||
| self.send_response(200) | ||
| self.send_header("Content-Type", "text/html; charset=utf-8") | ||
| self.send_header("Cache-Control", "no-store") | ||
| self.send_header("Content-Length", str(len(data))) | ||
| self.end_headers() | ||
| self.wfile.write(data) | ||
|
|
||
| def do_GET(self): | ||
| parsed = urlsplit(self.path) | ||
| if parsed.path == "/": | ||
| return self.page() | ||
|
|
||
| if parsed.path == "/paid": | ||
| session_id = (parse_qs(parsed.query).get("session_id") or [""])[0] | ||
| if not session_id: | ||
| return self.reply(400, {"error": "missing checkout session"}) | ||
| try: | ||
| session = stripe_session(session_id) | ||
| repo_url = paid_repo(session) | ||
| result = run_audit(repo_url) | ||
| result["paid"] = True | ||
|
Comment on lines
+97
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Every request containing the same paid Checkout Session ID reaches Useful? React with 👍 / 👎. |
||
| return self.reply(200, result) | ||
| except PermissionError as exc: | ||
| return self.reply(402, {"error": str(exc)}) | ||
| except ValueError as exc: | ||
| return self.reply(400, {"error": str(exc)}) | ||
| except Exception as exc: | ||
| print(f"paid audit failure: {exc!r}", flush=True) | ||
| return self.reply(500, {"error": "paid audit could not be verified or completed"}) | ||
|
|
||
| return self.reply(404, {"error": "not found"}) | ||
|
|
||
| def do_POST(self): | ||
| if self.path != "/audit": | ||
| if urlsplit(self.path).path != "/audit": | ||
| return self.reply(404, {"error": "not found"}) | ||
|
|
||
| try: | ||
| length = int(self.headers.get("Content-Length", 0)) | ||
| body = json.loads(self.rfile.read(min(length, 8192))) | ||
| repo_url = body["repo_url"] | ||
|
|
||
| if not REPO.fullmatch(repo_url): | ||
| return self.reply(400, {"error": "public github.com repository required"}) | ||
|
|
||
| with tempfile.TemporaryDirectory() as tmp: | ||
| target = os.path.join(tmp, "repo") | ||
| subprocess.run( | ||
| ["git", "clone", "--depth=1", repo_url, target], | ||
| check=True, | ||
| timeout=120, | ||
| ) | ||
| result = audit_path(target) | ||
| if length <= 0 or length > 8192: | ||
| return self.reply(400, {"error": "invalid request"}) | ||
| body = json.loads(self.rfile.read(length)) | ||
| repo_url = str(body["repo_url"]).strip() | ||
| result = run_audit(repo_url) | ||
|
|
||
| self.reply(200, result) | ||
| findings = result.get("findings") or [] | ||
| preview = { | ||
| "target": result.get("target"), | ||
| "surfaces": result.get("surfaces"), | ||
| "findings": findings[:FREE_FINDINGS], | ||
| "total_findings": len(findings), | ||
| "hmmm_count": len(result.get("hmmm") or []), | ||
| "locked": len(findings) > FREE_FINDINGS or bool(result.get("hmmm")), | ||
| "payment_link": PAYMENT_LINK_URL, | ||
| } | ||
| return self.reply(200, preview) | ||
|
|
||
| except (ValueError, KeyError, json.JSONDecodeError) as exc: | ||
| return self.reply(400, {"error": str(exc) or "invalid request"}) | ||
| except subprocess.TimeoutExpired: | ||
| return self.reply(504, {"error": "repository audit timed out"}) | ||
| except subprocess.CalledProcessError: | ||
| return self.reply(400, {"error": "repository could not be cloned"}) | ||
| except Exception as exc: | ||
| self.reply(500, {"error": str(exc)}) | ||
| print(f"audit failure: {exc!r}", flush=True) | ||
| return self.reply(500, {"error": "audit failed"}) | ||
|
|
||
|
|
||
| ThreadingHTTPServer( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Stripe Payment Link and paid server endpoint turn this distribution repository into a hosted SaaS, while the applicable work order explicitly lists “Do not host a SaaS” in
HANDOFF.md:26-30. This expands the repository beyond its clone/CLI scope and should be removed unless the repository contract is separately updated by an authorized change.AGENTS.md reference: AGENTS.md:L3-L5
Useful? React with 👍 / 👎.