Files
pegasusAPI/SERVICE_ARCHITECTURE.md

526 lines
19 KiB
Markdown
Raw Normal View History

2026-03-25 02:04:52 -05:00
# Service Architecture — Gateway Checker API
> Living document. Updated as we discuss and build. Read this before starting any work.
## Overview
REST API that wraps gateway checker scripts behind authenticated endpoints. Clients get an API key and hit endpoints — they never see source code. This is a **black-box service** — clients only know the API contract, not how it works internally.
### How the service works (the big picture)
```
┌─────────────────────────────────────────────────────────────────┐
│ YOUR SERVER │
│ │
│ Client A ──►┐ │
│ Client B ──►┤ FastAPI (api.py) │
│ Client C ──►┤ │ │
│ Tg Bot ──►┘ ├── Auth middleware (validate API key) │
│ ├── Rate limiter (per-key, per-minute) │
│ ├── Task queue (asyncio.Queue) │
│ │ │ │
│ │ ▼ │
│ ├── Gateway Registry │
│ │ ├── comwave_forbot.py [max_concurrent=1]│
│ │ ├── comwave_forbot.py [max_concurrent=5]│
│ │ └── future_forbot.py [max_concurrent=N]│
│ │ │
│ ├── Dedup cache (same card+gateway = cached) │
│ ├── Request logger (SQLite) │
│ └── DB (api.db) — keys, usage, logs │
│ │
│ Admin (you) ──► /admin/* endpoints (master key) │
└─────────────────────────────────────────────────────────────────┘
```
### The flow (step by step)
1. **Client sends request:** `POST /api/check/comwave` with API key + card
2. **Auth middleware:** Validates key exists, is active, not expired, not over limit
3. **Gateway access check:** Is this key allowed to use `comwave`?
4. **Rate limit check:** Has this key exceeded N requests/minute?
5. **Dedup check:** Same card + same gateway in last 60s? Return cached result
6. **Task created:** Card is queued, client gets back `task_id` immediately
7. **Worker picks it up:** Respects gateway's `max_concurrent` semaphore
8. **Gateway runs:** `comwave_forbot.py` does the actual check (5-15s)
9. **Result stored:** In-memory results dict + logged to SQLite
10. **Client polls:** `GET /api/result/{task_id}` → gets result when ready
---
## Stack
| Component | Choice | Why |
|---|---|---|
| Framework | **FastAPI** | Async native, auto-docs, fast |
| DB | **SQLite** (via `aiosqlite`) | Zero setup, file-based, good for this scale |
| Auth | API key in `X-API-Key` header | Simple, standard |
| Server | **uvicorn** | ASGI server for FastAPI |
| Config | **python-dotenv** | Already installed, clean `.env` loading |
| Keys | `secrets.token_urlsafe(32)` | 43-char random, unguessable |
| Task queue | **asyncio.Queue** | No Redis needed, in-process |
| Concurrency | **asyncio.Semaphore** per gateway | Controls parallel load |
**Packages to install:** `fastapi`, `uvicorn`, `aiosqlite`
---
## Files
| File | Purpose | Status |
|---|---|---|
| `api.py` | FastAPI server, gateway registry, auth, task queue, workers | ⬜ Not started |
| `db.py` | SQLite key management + request logging | ⬜ Not started |
| `comwave_forbot.py` | Comwave checker engine (existing, unchanged) | ✅ Done |
| `comwave_bot.py` | Telegram bot (existing, will update to call API later) | ✅ Done |
| `api.db` | SQLite database (auto-created at runtime) | Auto |
| `.env` | `MASTER_KEY`, `BOT_TOKEN`, `API_HOST`, `API_PORT`, credentials | ✅ Exists (needs `MASTER_KEY`) |
| `SERVICE_ARCHITECTURE.md` | This file | ✅ |
---
## API Endpoints
### Public (no auth)
| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/health` | Health check — `{"status": "ok", "uptime": 3600}` |
### Client Endpoints (require API key)
| Method | Endpoint | Body | Description |
|---|---|---|---|
| `POST` | `/api/check/{gateway}` | `{"card": "cc\|mm\|yyyy\|cvv"}` | Submit card for checking, returns `task_id` |
| `GET` | `/api/result/{task_id}` | — | Poll for result (queued/processing/completed) |
| `GET` | `/api/usage` | — | Check own usage/limits/expiry |
| `GET` | `/api/gateways` | — | List available gateways + status |
| `GET` | `/api/cooldown` | — | Check own cooldown timers per gateway |
### Admin Endpoints (require master key)
| Method | Endpoint | Body | Description |
|---|---|---|---|
| `POST` | `/admin/keys` | `{"owner", "gateways", "request_limit", "expires_days", "rate_per_minute"}` | Create new API key |
| `GET` | `/admin/keys` | — | List all keys with usage |
| `GET` | `/admin/keys/{key}` | — | Get single key details |
| `PATCH` | `/admin/keys/{key}` | fields to update | Update key limits/expiry/gateways |
| `DELETE` | `/admin/keys/{key}` | — | Revoke a key |
| `GET` | `/admin/stats` | — | Live server stats (queues, 24h counts, gateway health) |
---
## Gateway Registry
Each gateway declares its function, concurrency limit, timeout, and optional lifecycle hooks:
```python
GATEWAYS = {
"comwave": {
"fn": checker.check_card, # the async check function
"type": "$0 auth", # display label
"status": "online", # online/maintenance/offline
"max_concurrent": 1, # uses shared session, serialize
"timeout": 30, # seconds before giving up
"cooldown": 20, # per-user cooldown in seconds
"max_queue": 50, # max tasks waiting in queue
"init": checker.ensure_logged_in, # called on API startup
"shutdown": checker.shutdown, # called on API shutdown
},
"comwave3": {
"fn": check_card_3,
"type": "$3.33 charge",
"status": "online",
"max_concurrent": 5, # stateless, can run in parallel
"timeout": 60,
"cooldown": 20, # per-user cooldown in seconds
"max_queue": 50, # max tasks waiting in queue
"init": None,
"shutdown": None,
},
# Adding a new gateway:
# "duke": {
# "fn": duke_check,
# "type": "$1.00 charge",
# "status": "online",
# "max_concurrent": 3,
# "timeout": 45,
# "cooldown": 20,
# "max_queue": 50,
# "init": None,
# "shutdown": None,
# },
}
```
**Adding a new gateway:**
1. Code `new_site_forbot.py` with `async def check_card(card: str) -> dict` function
2. Import and register in `GATEWAYS` dict in `api.py`
3. Create/update client keys to include the new gateway name
4. Done — client uses `POST /api/check/newsite`
**Gateway function contract:**
```python
async def check_card(card: str) -> dict:
# card = "cc|mm|yyyy|cvv"
# Must return: {"status": "approved"|"declined"|"error", "message": "...", "time": float}
```
**Cooldown:**
- Each gateway has a `cooldown` value (seconds) — **per-user**, not global
- After a user submits a card to a gateway, they must wait `cooldown` seconds before submitting another to the **same** gateway
- Different users are independent — User A's cooldown doesn't block User B
- Different gateways are independent — cooldown on `comwave` doesn't affect `comwave3`
- Tracked via in-memory dict: `last_request[api_key][gateway] → timestamp`
- If a user submits too soon: `429 {"detail": "Cooldown active", "retry_after": 12.5}` (remaining seconds)
- Trade-off accepted: site load scales with number of active users (10 users = up to 10 concurrent requests), but users never wait for each other
**Concurrency control:**
- Each gateway gets its own `asyncio.Semaphore(max_concurrent)`
- `comwave` max_concurrent=1 → only 1 check at a time (shared session with lock)
- `comwave3` max_concurrent=5 → up to 5 parallel checks (stateless)
- Excess requests wait in queue, not rejected
**Lifecycle hooks:**
- `init()` — called once when API server starts (e.g. comwave login)
- `shutdown()` — called once when API server stops (e.g. comwave logout)
- Both are optional (`None` for stateless gateways)
---
## Task Queue System
Instead of making clients wait 5-15s for a response, we use async tasks:
### Submit flow
```
POST /api/check/comwave {"card": "4111...|12|2025|123"}
Returns immediately:
{"task_id": "abc123", "status": "queued", "gateway": "comwave", "position": 2, "estimated_wait": 15}
```
### Poll flow
```
GET /api/result/abc123
While queued: {"task_id": "abc123", "status": "queued", "position": 2, "expires_in": 290}
While processing: {"task_id": "abc123", "status": "processing", "expires_in": 270}
When done: {"task_id": "abc123", "status": "completed", "result": {...}, "remaining": 999}
```
### Implementation
- `asyncio.Queue` per gateway — tasks are queued and processed by workers
- Workers respect `max_concurrent` semaphore
- Results stored in `dict[task_id] → result` (in-memory, TTL 5 minutes)
- `task_id` = `uuid4()` short string
### Why this is better than synchronous
- No HTTP timeout issues (client gets response in <100ms)
- Client can submit multiple cards and poll all results
- Server controls processing speed via semaphores
- Batch support becomes trivial (submit N tasks, poll N results)
### Task ownership
- Each task is tied to the API key that created it
- Only the creating key can poll `GET /api/result/{task_id}`
- Other keys get `404 "Task not found"` — prevents cross-client result snooping
- Stored as `tasks[task_id] = {"api_key": key, "result": ..., ...}`
### Max queue depth
- Each gateway has a max queue size (default: 50 tasks)
- If queue is full: `503 {"detail": "Gateway overloaded", "queue_depth": 50}`
- Prevents a single client from flooding the queue during cooldown edge cases
- Configurable per gateway via `max_queue` field in registry
---
## Throttling Strategy
**Model: Per-user cooldown (20s default)**
Each user (API key) has an independent cooldown timer per gateway. After submitting a card, that user must wait `cooldown` seconds before submitting another card to the same gateway.
### How it works
```
User A submits card → comwave processes → User A must wait 20s
User B submits card → comwave processes → User B must wait 20s (independent of A)
User A submits to comwave3 → allowed immediately (different gateway)
```
### Implementation
```python
# In-memory tracker
last_request: dict[str, dict[str, float]] = {} # {api_key: {gateway: timestamp}}
# Before queuing a task:
now = time.time()
last = last_request.get(api_key, {}).get(gateway, 0)
remaining = cooldown - (now - last)
if remaining > 0:
raise HTTPException(429, {"detail": "Cooldown active", "retry_after": round(remaining, 1)})
last_request.setdefault(api_key, {})[gateway] = now
```
### Behavior with multiple users
| Time | User A | User B | User C | Site load |
|------|--------|--------|--------|-----------|
| 0s | submits card | submits card | — | 2 concurrent |
| 5s | waiting (15s left) | waiting (15s left) | submits card | 1 concurrent |
| 20s | submits next | submits next | waiting (5s left) | 2 concurrent |
| 25s | waiting | waiting | submits next | 1 concurrent |
**Max site load = number of active users** (each can send 1 request per 20s)
---
## Request Deduplication
If a client sends the **same card** to the **same gateway** within **60 seconds**, return the cached result instead of hitting the gateway again.
- **Key:** `hash(card + gateway)` (SHA256, not stored in plain text)
- **Cache:** In-memory dict with TTL
- **Why:** Prevents accidental double-charges on `comwave3`, saves gateway resources
---
## Database Schema (SQLite)
### `api_keys` table
| Column | Type | Description |
|---|---|---|
| `api_key` | TEXT PK | The key string (`sk_live_...`) |
| `owner` | TEXT | Client name/identifier |
| `created_at` | DATETIME | When key was created |
| `expires_at` | DATETIME NULL | NULL = never expires |
| `request_limit` | INTEGER NULL | NULL = unlimited |
| `requests_used` | INTEGER | Counter, starts at 0 |
| `rate_per_minute` | INTEGER | Max requests per minute (default 10) |
| `allowed_gateways` | TEXT | JSON list `["comwave","comwave3"]` or `"*"` for all |
| `is_active` | BOOLEAN | Can be deactivated without deleting |
| `is_paused` | BOOLEAN | Temporarily disabled (client sees "API key paused") |
### `request_log` table
| Column | Type | Description |
|---|---|---|
| `id` | INTEGER PK | Auto-increment |
| `api_key` | TEXT | Which key was used |
| `gateway` | TEXT | Which gateway |
| `card` | TEXT | Full card (`cc|mm|yyyy|cvv`) |
| `status` | TEXT | approved/declined/error |
| `response_time` | REAL | Seconds |
| `created_at` | DATETIME | Timestamp |
| `ip_address` | TEXT | Client IP |
### `admin_log` table
| Column | Type | Description |
|---|---|---|
| `id` | INTEGER PK | Auto-increment |
| `action` | TEXT | create_key / revoke_key / update_key / pause_key / unpause_key |
| `target_key` | TEXT | Which API key was affected |
| `details` | TEXT | JSON of what changed (e.g. `{"request_limit": [1000, 2000]}`) |
| `ip_address` | TEXT | Admin's IP |
| `created_at` | DATETIME | Timestamp |
---
## Security
| Layer | Implementation |
|---|---|
| **Auth** | `X-API-Key` header required on all client/admin endpoints |
| **Key format** | `sk_live_` + `secrets.token_urlsafe(32)` |
| **No docs in prod** | `FastAPI(docs_url=None, redoc_url=None)` |
| **HTTPS** | Behind nginx with SSL or Cloudflare tunnel |
| **Rate limit** | Per-minute limit per API key (in-memory sliding window) |
| **IP block** | Block IP after 10 failed auth attempts (in-memory, resets after 15min) |
| **Full card logging** | Full card stored in request_log (keep `api.db` protected) |
| **Dedup hashing** | Card+gateway hashed with SHA256, never stored raw |
| **Master key** | From `.env`, used for `/admin/*` only |
| **Health endpoint** | `/health` is the only unauthenticated endpoint, reveals nothing sensitive |
| **Graceful shutdown** | Finish in-progress checks before exit, call gateway shutdown hooks |
---
## Response Format
### Submit check
```json
{
"task_id": "a1b2c3d4",
"status": "queued",
"gateway": "comwave",
"position": 2,
"estimated_wait": 15
}
```
### Poll result (queued)
```json
{
"task_id": "a1b2c3d4",
"status": "queued",
"position": 2,
"expires_in": 290
}
```
### Poll result (processing)
```json
{
"task_id": "a1b2c3d4",
"status": "processing",
"expires_in": 270
}
```
### Poll result (completed)
```json
{
"task_id": "a1b2c3d4",
"status": "completed",
"result": {
"status": "declined",
"gateway": "comwave",
"message": "DECLINED [VISA] | Last4: 1111 | Auth: 000000 | Ref: 662321650018600120",
"time": 5.28
},
"remaining": 999
}
```
### Usage
```json
{
"owner": "client_john",
"requests_used": 347,
"requests_limit": 1000,
"expires_at": "2026-04-24T00:00:00",
"gateways": ["comwave", "comwave3"],
"rate_per_minute": 10
}
```
### Gateways
```json
{
"gateways": {
"comwave": {"status": "online", "type": "$0 auth"},
"comwave3": {"status": "online", "type": "$3.33 charge"}
}
}
```
### Errors
```json
{"detail": "Missing API key"} // 401
{"detail": "Invalid API key"} // 401
{"detail": "API key expired"} // 401
{"detail": "API key deactivated"} // 401
{"detail": "Request limit exceeded", "used": 1000, "limit": 1000} // 429
{"detail": "Rate limit exceeded", "retry_after": 6} // 429
{"detail": "Cooldown active", "retry_after": 12.5} // 429
{"detail": "Access denied for gateway: duke"} // 403
{"detail": "Gateway not found: xyz"} // 404
{"detail": "Gateway offline: duke"} // 503
{"detail": "Gateway overloaded", "queue_depth": 50} // 503
{"detail": "API key paused"} // 401
{"detail": "Invalid card format"} // 400
{"detail": "Task not found"} // 404
{"detail": "IP blocked"} // 403
```
---
## Key Tiers (pricing model)
| Tier | Limit | Expiry | Gateways | Rate |
|---|---|---|---|---|
| Trial | 50 requests | 7 days | 1 gateway | 5/min |
| Standard | 1000/month | 30 days | specific | 10/min |
| Unlimited | no limit | no expiry | all | 50/min |
---
## Telegram Bot Integration (future)
**Option A (recommended):** Bot calls own API with its own key — cleanest separation, bot is just another API consumer. The bot gets an "unlimited" key with all gateways.
**Option B:** Bot imports checkers directly (current setup) — faster, no network hop but tightly coupled.
Decision: TBD — build API first, connect bot later.
---
## .env Structure
```env
# API Server
MASTER_KEY=your_admin_secret_here
API_HOST=0.0.0.0
API_PORT=8000
# Telegram Bot
BOT_TOKEN=your_bot_token
ALLOWED_USERS=123456,789012
# Gateway Credentials
COMWAVE_USERNAME=tu76502@gmail.com
COMWAVE_PASSWORD=root13579
```
---
## Build Order
### Phase 1: Core API (v1)
1. ⬜ Install `fastapi`, `uvicorn`, `aiosqlite`
2. ⬜ Build `db.py` — SQLite init, key CRUD, usage tracking, request logging
3. ⬜ Build `api.py` — FastAPI app, auth middleware, gateway registry, task queue + workers, all endpoints
4. ⬜ Test with comwave gateway — submit, poll, verify result
5. ⬜ Test admin endpoints — create key, list, revoke
6. ⬜ Test security — expired key, over-limit, wrong gateway, IP blocking
7. ⬜ Test concurrency — multiple clients hitting same gateway
### Phase 2: Bot + Polish
8. ⬜ Connect Telegram bot via API (Option A)
9. ⬜ Add more gateways as clients request them
### Phase 3: Future (v2)
10. ⬜ Webhook/callback support for async results
11. ⬜ Batch endpoint (`POST /api/check/batch/{gateway}`)
12. ⬜ IP whitelisting per API key
13. ⬜ Key rotation
14. ⬜ Read-only key scoping (usage-only vs full-access)
15. ⬜ Client dashboard (simple HTML)
16. ⬜ Gateway health monitoring (auto-disable on failure)
17. ⬜ Cloudflare Tunnel for easy HTTPS
---
## Existing Gateways Reference
### Comwave $0 auth (`comwave`)
- **File:** `comwave_forbot.py``ComwaveChecker.check_card()`
- **Type:** Account portal card update, Moneris Checkout v1
- **Amount:** $0 (auth only)
- **Session:** Single persistent login, `asyncio.Lock` for ticket serialization
- **Concurrency:** max_concurrent=1 (shared session)
- **Lifecycle:** init = `ensure_logged_in()`, shutdown = `shutdown()`
- **Note:** Server enforces one login at a time
### Comwave $3.33 charge (`comwave3`)
- **File:** `comwave_forbot.py``check_card_3()`
- **Type:** WooCommerce guest checkout, Moneris Checkout v1
- **Amount:** $3.33 CAD ($2.95 + 12% tax)
- **Session:** Stateless, each call gets own session
- **Concurrency:** max_concurrent=5 (fully parallel-safe)
- **Lifecycle:** no init/shutdown needed
---
*Last updated: 2026-03-24*