Metadata-Version: 2.4
Name: gkcaptcha
Version: 0.1.0
Summary: Python SDK for gkCAPTCHA - AI-resistant CAPTCHA verification
Author-email: Gatekeeper <dev@gatekeeper.sa>
License: MIT
Keywords: captcha,gkcaptcha,bot-detection,security
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.25.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"

# gkcaptcha

Python SDK for [gkCAPTCHA](https://gatekeeper.sa) — AI-resistant CAPTCHA verification for backend services.

## Quick Start

```python
from gkcaptcha import GkCaptchaClient

client = GkCaptchaClient(
    secret_key="sk_live_...",
    site_key="pk_live_...",
)

result = client.verify_token(request.POST.get("captchaToken"))
if not result.success:
    return HttpResponseForbidden("CAPTCHA verification failed")
```

## Installation

```bash
pip install gkcaptcha
```

**Requirements:** Python 3.9+, `httpx>=0.25.0`

## Async Support

```python
import asyncio
from gkcaptcha import GkCaptchaClient

client = GkCaptchaClient(secret_key="sk_live_...", site_key="pk_live_...")

async def verify(token: str):
    result = await client.verify_token_async(token)
    return result.success
```

## Configuration

### Constructor Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `secret_key` | `str` | env `GKCAPTCHA_SECRET_KEY` | Your secret key |
| `site_key` | `str` | env `GKCAPTCHA_SITE_KEY` | Your site key |
| `api_url` | `str` | env `GKCAPTCHA_API_URL` or `https://gkcaptcha.gatekeeper.sa` | API base URL |
| `timeout` | `float` | `5.0` | Request timeout in seconds |
| `max_retries` | `int` | `1` | Max retries on network error |
| `retry_delay` | `float` | `1.0` | Seconds between retries |
| `fail_closed` | `bool` | `False` | Raise on network error instead of fail-open |

### Environment Variables

```bash
export GKCAPTCHA_SECRET_KEY=sk_live_...
export GKCAPTCHA_SITE_KEY=pk_live_...

# Then construct without arguments:
client = GkCaptchaClient()
```

## Fail-Open vs Fail-Closed

**Default behavior (fail-open):** If the gkCAPTCHA API is unreachable due to a network error, verification returns `VerifyTokenResponse(success=True, fail_open=True)`. This protects legitimate users during API outages.

```python
client = GkCaptchaClient(secret_key="...", site_key="...")
result = client.verify_token(token)
if result.fail_open:
    # Network error occurred — request was allowed through
    log.warning("CAPTCHA verification skipped due to network error")
```

**Fail-closed mode:** Raises `GkCaptchaError` on any network failure.

```python
client = GkCaptchaClient(secret_key="...", site_key="...", fail_closed=True)
try:
    result = client.verify_token(token)
except GkCaptchaError as e:
    if e.code == "NETWORK_ERROR":
        return HttpResponse("Service temporarily unavailable", status=503)
    raise
```

**Timeout always raises** regardless of `fail_closed` setting — a timeout indicates a configuration problem, not transient instability.

> **Note:** Unlike other SDKs, Python raises `GkCaptchaError` on timeout instead of retrying. Set a longer timeout if needed.

## Error Codes

| Code | When |
|------|------|
| `TIMEOUT` | Request exceeded `timeout` seconds. Always raises. |
| `NETWORK_ERROR` | Network failure after all retries. Only raises when `fail_closed=True`. |
| `INVALID_CONFIG` | Missing `secret_key` or `site_key` at construction time. |

## Response Fields

```python
@dataclass(frozen=True)
class VerifyTokenResponse:
    success: bool           # True = human, False = bot/failed
    score: float | None     # Risk score 0.0 (human) to 1.0 (bot)
    timestamp: int | None   # Unix timestamp of verification
    error: str | None       # Error message if success=False
    reason_code: str | None # Machine-readable failure reason
    fail_open: bool         # True if network error caused pass-through
```

## Framework Integration Examples

### Django View

```python
# views.py
from django.http import HttpResponseForbidden
from gkcaptcha import GkCaptchaClient

client = GkCaptchaClient()  # reads from environment

def my_form_view(request):
    if request.method == "POST":
        token = request.POST.get("captchaToken", "")
        result = client.verify_token(token, options=VerifyOptions(
            client_ip=request.META.get("REMOTE_ADDR"),
            user_agent=request.META.get("HTTP_USER_AGENT"),
        ))
        if not result.success:
            return HttpResponseForbidden("CAPTCHA verification failed")
        # Process form...
```

### Django Decorator (copy-paste, not shipped code)

```python
# decorators.py
from functools import wraps
from django.http import HttpResponseForbidden
from gkcaptcha import GkCaptchaClient

_client = GkCaptchaClient()

def require_captcha(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        token = request.POST.get("captchaToken", "")
        result = _client.verify_token(token)
        if not result.success:
            return HttpResponseForbidden("CAPTCHA verification failed")
        return view_func(request, *args, **kwargs)
    return wrapper

# Usage:
@require_captcha
def register(request):
    ...
```

### FastAPI Dependency Injection (copy-paste, not shipped code)

```python
# captcha.py
from fastapi import Depends, HTTPException, Request
from gkcaptcha import GkCaptchaClient, GkCaptchaError, VerifyOptions

client = GkCaptchaClient()  # reads from environment

async def verify_captcha(
    request: Request,
    captcha_token: str,
) -> None:
    options = VerifyOptions(
        client_ip=request.client.host,
        user_agent=request.headers.get("user-agent"),
    )
    try:
        result = await client.verify_token_async(captcha_token, options=options)
    except GkCaptchaError as e:
        if e.code == "TIMEOUT":
            raise HTTPException(status_code=503, detail="CAPTCHA service timeout")
        raise
    if not result.success:
        raise HTTPException(status_code=400, detail="CAPTCHA verification failed")

# Usage in route:
@app.post("/register")
async def register(
    form: RegisterForm,
    _: None = Depends(verify_captcha),
):
    ...
```

## Widget Integration

Add the gkCAPTCHA widget to your HTML form:

```html
<form method="POST">
    <gk-captcha sitekey="pk_live_..."></gk-captcha>
    <button type="submit">Submit</button>
</form>

<script src="https://gatekeeper.sa/widget.js" async></script>
```

The widget emits a `captchaToken` field on form submission. Pass this to `verify_token()` on your backend.

## License

MIT
