From e3f4d13e9a1f7f6ab62cfec7fac36917e7bfd01a Mon Sep 17 00:00:00 2001 From: Oscar Quiloulou Date: Mon, 18 May 2026 09:06:16 +0000 Subject: [PATCH 1/2] fix(render): update provider to support OAuth accounts and new Render GraphQL behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render removed all legacy email validation mutations (validateEmail, checkEmail, validateSignup). The only remaining mutation is `signUp`, which returns structured errors indicating the email state. This patch updates the provider to use `signUp` and correctly interpret all possible responses: - {"email":"exists"} → classic Render account (email+password) - {"email":"invalid"} → OAuth-only account (GitHub/Google) → treat as taken - {"hcaptcha_token":"invalid"} → valid but unused email → available Also adds required x-render-client-* headers to avoid GraphQL validation errors. This makes the Render provider fully functional again and compatible with both classic and OAuth-based accounts. --- tests/test_api_render.py | 96 +++++++++++++++++++++++ user_scanner/email_scan/hosting/render.py | 66 ++++++++++------ 2 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 tests/test_api_render.py diff --git a/tests/test_api_render.py b/tests/test_api_render.py new file mode 100644 index 00000000..10ce2948 --- /dev/null +++ b/tests/test_api_render.py @@ -0,0 +1,96 @@ +import httpx +import asyncio + +URL = "https://api.render.com/graphql" + +HEADERS = { + "User-Agent": "Mozilla/5.0", + "Content-Type": "application/json", + "origin": "https://dashboard.render.com", + "referer": "https://dashboard.render.com/register", + + # Headers internes obligatoires + "x-render-client-name": "dashboard", + "x-render-client-version": "1.0.0", + "x-render-client-platform": "web", + "x-render-client-build": "production", + "x-render-client-release-channel": "stable" +} + +EMAIL = "dimitri.acteur@outlook.com" + +MUTATIONS = { + "validateEmail": { + "query": """ + mutation validateEmail($email: String!) { + validateEmail(email: $email) { + valid + exists + } + } + """, + "variables": {"email": EMAIL} + }, + + "checkEmail": { + "query": """ + mutation checkEmail($email: String!) { + checkEmail(email: $email) + } + """, + "variables": {"email": EMAIL} + }, + + "validateSignup": { + "query": """ + mutation validateSignup($signup: SignupInput!) { + validateSignup(signup: $signup) + } + """, + "variables": {"signup": {"email": EMAIL}} + }, + + "signUp": { + "query": """ + mutation signUp($signup: SignupInput!) { + signUp(signup: $signup) { + idToken + } + } + """, + "variables": { + "signup": { + "email": EMAIL, + "password": "Test123!", + "inviteCode": "" + } + } + } +} + + +async def test_mutation(name, mutation): + print(f"\n=== Testing mutation: {name} ===") + + payload = { + "operationName": name, + "query": mutation["query"], + "variables": mutation["variables"] + } + + async with httpx.AsyncClient(http2=True, timeout=5) as client: + try: + r = await client.post(URL, json=payload, headers=HEADERS) + print("Status:", r.status_code) + print("Response:", r.text) + except Exception as e: + print("Error:", e) + + +async def main(): + for name, mutation in MUTATIONS.items(): + await test_mutation(name, mutation) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/user_scanner/email_scan/hosting/render.py b/user_scanner/email_scan/hosting/render.py index 6de39860..9b454c0e 100644 --- a/user_scanner/email_scan/hosting/render.py +++ b/user_scanner/email_scan/hosting/render.py @@ -1,4 +1,5 @@ import httpx +import json from user_scanner.core.result import Result @@ -11,49 +12,64 @@ async def _check(email: str) -> Result: "variables": { "signup": { "email": email, - "githubId": "", - "name": "", - "githubToken": "", - "googleId": "", - "gitlabId": "", - "bitbucketId": "", - "inviteCode": "", - "password": "StandardPassword123!", - "newsletterOptIn": False, - "next": "" + "password": "Test123!", + "inviteCode": "" } }, - "query": "mutation signUp($signup: SignupInput!) {\n signUp(signup: $signup) {\n idToken\n __typename\n }\n}\n" + "query": """ + mutation signUp($signup: SignupInput!) { + signUp(signup: $signup) { + idToken + } + } + """ } headers = { - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0", "Content-Type": "application/json", "origin": "https://dashboard.render.com", "referer": "https://dashboard.render.com/register", - "accept-language": "en-US,en;q=0.9" + + # Headers internes obligatoires + "x-render-client-name": "dashboard", + "x-render-client-version": "1.0.0", + "x-render-client-platform": "web", + "x-render-client-build": "production", + "x-render-client-release-channel": "stable" } - async with httpx.AsyncClient(http2=True, timeout=4.0) as client: + async with httpx.AsyncClient(http2=True, timeout=5) as client: try: response = await client.post(url, json=payload, headers=headers) + data = response.json() - if response.status_code == 429: - return Result.error("Rate limited, use '-d' flag to avoid bot detection") + # Render renvoie toujours une erreur dans "errors" + if "errors" in data: + raw = data["errors"][0]["message"] - data = response.json() - errors = data.get("errors", []) + # Le message est un JSON encodé dans une string + try: + msg = json.loads(raw) + except: + return Result.error(f"Render Error: {raw}") - if errors: - msg = errors[0].get("message", "") - if '"email":"exists"' in msg: + # 1) Compte Render classique + if msg.get("email") == "exists": return Result.taken(url=show_url) - elif '"hcaptcha_token":"invalid"' in msg: + + # 2) Compte OAuth (GitHub / Google) + # Render renvoie "invalid" car l'email n'est pas dans la base "password users" + if msg.get("email") == "invalid": + return Result.taken(url=show_url) + + # 3) Email valide mais non existant + if msg.get("hcaptcha_token") == "invalid": return Result.available(url=show_url) - else: - return Result.error(f"Render Error: {msg}") - return Result.error("Unexpected error, report it via GitHub issues") + return Result.error(f"Render Error: {msg}") + + return Result.error("Unexpected response format from Render") except Exception as e: return Result.error(e) From 17be7d0e38d8b7809284e868479f0927f4192851 Mon Sep 17 00:00:00 2001 From: Oscar Quiloulou Date: Mon, 18 May 2026 09:37:22 +0000 Subject: [PATCH 2/2] chore: remove obsolete test_api_render.py --- tests/test_api_render.py | 96 ---------------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 tests/test_api_render.py diff --git a/tests/test_api_render.py b/tests/test_api_render.py deleted file mode 100644 index 10ce2948..00000000 --- a/tests/test_api_render.py +++ /dev/null @@ -1,96 +0,0 @@ -import httpx -import asyncio - -URL = "https://api.render.com/graphql" - -HEADERS = { - "User-Agent": "Mozilla/5.0", - "Content-Type": "application/json", - "origin": "https://dashboard.render.com", - "referer": "https://dashboard.render.com/register", - - # Headers internes obligatoires - "x-render-client-name": "dashboard", - "x-render-client-version": "1.0.0", - "x-render-client-platform": "web", - "x-render-client-build": "production", - "x-render-client-release-channel": "stable" -} - -EMAIL = "dimitri.acteur@outlook.com" - -MUTATIONS = { - "validateEmail": { - "query": """ - mutation validateEmail($email: String!) { - validateEmail(email: $email) { - valid - exists - } - } - """, - "variables": {"email": EMAIL} - }, - - "checkEmail": { - "query": """ - mutation checkEmail($email: String!) { - checkEmail(email: $email) - } - """, - "variables": {"email": EMAIL} - }, - - "validateSignup": { - "query": """ - mutation validateSignup($signup: SignupInput!) { - validateSignup(signup: $signup) - } - """, - "variables": {"signup": {"email": EMAIL}} - }, - - "signUp": { - "query": """ - mutation signUp($signup: SignupInput!) { - signUp(signup: $signup) { - idToken - } - } - """, - "variables": { - "signup": { - "email": EMAIL, - "password": "Test123!", - "inviteCode": "" - } - } - } -} - - -async def test_mutation(name, mutation): - print(f"\n=== Testing mutation: {name} ===") - - payload = { - "operationName": name, - "query": mutation["query"], - "variables": mutation["variables"] - } - - async with httpx.AsyncClient(http2=True, timeout=5) as client: - try: - r = await client.post(URL, json=payload, headers=HEADERS) - print("Status:", r.status_code) - print("Response:", r.text) - except Exception as e: - print("Error:", e) - - -async def main(): - for name, mutation in MUTATIONS.items(): - await test_mutation(name, mutation) - - -if __name__ == "__main__": - asyncio.run(main())