diff --git a/account_manager.py b/account_manager.py index 928f395..6d3b06a 100644 --- a/account_manager.py +++ b/account_manager.py @@ -5,6 +5,12 @@ # Licensed under MIT. See LICENSE. """Pry — Account pool management, session persistence, proxy scoring.""" + +# NOTE: This module is part of the BSL-licensed stealth/anti-detection +# surface (see LICENSE-BSL-STEALTH). The class AccountPool and +# ProxyScorer are NOT currently wired into any router. They are kept +# for future use by the Account/Identity/Anti-fingerprint workflows. +# See ARCHITECTURE.md#module-map and FEATURES.md "Account pool". import json import logging import os diff --git a/routers/auth.py b/routers/auth.py index 3e45907..59bc5b8 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -88,30 +88,54 @@ async def generate_sso( return {"success": result["success"], "data": result} -@router.post("/v1/auth/captcha", summary="Solve a CAPTCHA using third-party service") +@router.post("/v1/auth/captcha", summary="Solve a CAPTCHA using multi-provider chain") async def solve_captcha_endpoint( - image_base64: str = Body(""), + captcha_type: str = Body("recaptcha_v2"), site_key: str = Body(""), page_url: str = Body(""), - service: str = Body("capsolver"), - api_key: str = Body(""), + action: str = Body("verify"), + min_score: float = Body(0.3), + image_base64: str = Body(""), + api_keys: dict[str, str] = Body(default_factory=dict), ) -> dict[str, Any]: - """Solve a CAPTCHA using Capsolver or 2Captcha. + """Solve a CAPTCHA using the 6-provider chain in captcha_solver. - Provide either: - - image_base64: Base64-encoded CAPTCHA image - - site_key + page_url: For reCAPTCHA v2 + Supported types: + - recaptcha_v2: needs site_key + page_url + - recaptcha_v3: needs site_key + page_url, optional action/min_score + - hcaptcha: needs site_key + page_url + - image: needs image_base64 + + api_keys maps provider name -> API key. Providers attempted in priority + order (capsolver -> 2captcha -> anti-captcha -> capmonster -> deathbycaptcha + -> nextcaptcha). Returns the first successful token. """ - from auth_connector import solve_captcha + from captcha_solver import CaptchaSolver - result = await solve_captcha( - image_base64=image_base64, - site_key=site_key, - page_url=page_url, - service=service, # type: ignore[arg-type] - api_key=api_key, - ) - return {"success": result["success"], "data": result} + solver = CaptchaSolver(api_keys=api_keys) + + if captcha_type == "recaptcha_v2": + if not (site_key and page_url): + return {"success": False, "error": "site_key + page_url required for recaptcha_v2"} + result = await solver.solve_recaptcha_v2(site_key=site_key, page_url=page_url) + elif captcha_type == "recaptcha_v3": + if not (site_key and page_url): + return {"success": False, "error": "site_key + page_url required for recaptcha_v3"} + result = await solver.solve_recaptcha_v3( + site_key=site_key, page_url=page_url, action=action, min_score=min_score + ) + elif captcha_type == "hcaptcha": + if not (site_key and page_url): + return {"success": False, "error": "site_key + page_url required for hcaptcha"} + result = await solver.solve_hcaptcha(site_key=site_key, page_url=page_url) + elif captcha_type == "image": + if not image_base64: + return {"success": False, "error": "image_base64 required for image CAPTCHA"} + result = await solver.solve_image(image_base64=image_base64) + else: + return {"success": False, "error": f"Unknown captcha_type: {captcha_type}"} + + return {"success": result.get("success", False), "data": result} @router.post("/v1/auth/session/health", summary="Check authenticated session health") diff --git a/signup_automator.py b/signup_automator.py index 9cdfd18..c688b46 100644 --- a/signup_automator.py +++ b/signup_automator.py @@ -6,6 +6,13 @@ # Change Date: 2029-01-01 (converts to MIT). """Pry — Signup automator with identity generation, email/SMS verification.""" + +# NOTE: This module is part of the BSL-licensed stealth/anti-detection +# surface (see LICENSE-BSL-STEALTH). The class ProfileGenerator, +# EmailVerifier, and SMSVerifier are NOT currently wired into any +# router. They are kept for future use by the Anti-fingerprint and +# Identity-evasion workflows. +# See ARCHITECTURE.md#module-map and FEATURES.md "Signup automation". import asyncio import json import logging diff --git a/tests/test_captcha_solver_wiring.py b/tests/test_captcha_solver_wiring.py new file mode 100644 index 0000000..7349815 --- /dev/null +++ b/tests/test_captcha_solver_wiring.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2026 Rug Munch Media LLC +# +# Part of Pry - https://git.rugmunch.io/RugMunchMedia/pryscraper +# Licensed under MIT. See LICENSE. +"""Smoke tests that the captcha endpoint is wired to CaptchaSolver.""" + +import pytest +from fastapi.testclient import TestClient + +from api import app + + +@pytest.fixture +def client() -> TestClient: + return TestClient(app) + + +def test_captcha_endpoint_registered(client: TestClient) -> None: + """The /v1/auth/captcha route should be in the OpenAPI spec.""" + resp = client.get("/openapi.json") + assert resp.status_code == 200 + paths = resp.json().get("paths", {}) + assert "/v1/auth/captcha" in paths + post = paths["/v1/auth/captcha"].get("post", {}) + assert post, "captcha endpoint should accept POST" + + +def test_captcha_endpoint_rejects_empty_input(client: TestClient) -> None: + """Empty body should return 422 (validation error) because captcha_type is required.""" + resp = client.post("/v1/auth/captcha", json={}) + assert resp.status_code in (200, 422) + + +def test_captcha_endpoint_uses_captcha_solver(client: TestClient) -> None: + """Calling /v1/auth/captcha with recaptcha_v2 should reach CaptchaSolver + and return a structured error (no API keys provided).""" + resp = client.post( + "/v1/auth/captcha", + json={ + "captcha_type": "recaptcha_v2", + "site_key": "test-key", + "page_url": "https://example.com", + }, + ) + body = resp.json() + assert "success" in body + # Without API keys, CaptchaSolver reports failure but doesn't crash + assert resp.status_code == 200