randomayzer/docs/PRODUCTION_GUARDS.md

105 lines
6.2 KiB
Markdown

# Randomayzer — Production Guards & Security Architecture
This document details the production integrity guards, concurrency safeguards, rate limiting, and idempotency semantics implemented in **Randomayzer**.
---
## 1. Idempotency Hardening & Request Fingerprinting
### Contract
The API supports the standard `Idempotency-Key` HTTP header on state-mutating endpoints:
- `POST /api/giveaways` (Giveaway creation)
- `POST /api/giveaways/[id]/participants` (Participant import & enrichment)
- `POST /api/giveaways/[id]/snapshot` (Participant snapshot locking)
### Semantics
1. **Scoped Storage Key**:
Keys are composite and scoped by `operation`, `giveawayId`, and `idempotencyKey`:
`format: ${operation}:${giveawayId || 'global'}:${key}`
This prevents cross-endpoint and cross-giveaway key collision.
2. **Request Fingerprinting**:
Every request computes a canonical SHA-256 fingerprint:
`requestFingerprint = SHA256(canonicalStringify(requestPayload))`
3. **Replay vs Conflict Handling**:
- **Same Key + Same Request**: Returns the previously cached status code and response body without re-executing.
- **Same Key + Different Request**: Throws HTTP `409 Conflict` with error code `IDEMPOTENCY_KEY_REUSED`.
4. **Key Validation & TTL**:
- Maximum key length is 128 characters (exceeding length returns `400 VALIDATION_ERROR`).
- Default TTL is 5 minutes with proactive cleanup and upper memory bounds.
---
## 2. Rate Limiting & Client Identity Resolution
### Client Identity Scoping Architecture
- **Authenticated Routes (`/api/giveaways*`)**:
Rate limits are keyed strictly by trusted server-side `sessionUser.id` (e.g. `draw-execute:${sessionUser.id}:${id}`, `giveaways-list:${sessionUser.id}`) **after** session authentication and ownership checks.
This ensures:
1. Different organizers have isolated rate limit buckets and never block each other, even when `req.ip` is unpopulated.
2. Unauthenticated attackers receive `401 Unauthorized` before reaching the rate limiter and cannot drain any organizer's quota.
- **Anonymous / Hybrid Routes (`/api/posts/preview`, `/api/auth/vk/start`, `/api/giveaways/[id]/verify`)**:
- `POST /api/posts/preview`: Uses `post-preview:user:${sessionUser.id}` if a valid session exists, and `post-preview:anon:${clientIp}` if anonymous. An anonymous attacker consuming the IP limit cannot affect authenticated organizers.
- `GET /api/auth/vk/start`: Rate-limited per resolved client IP (`oauth-start:${clientIp}`).
- `GET /api/giveaways/[id]/verify`: Rate-limited per resolved client IP and giveaway (`verify-get:${clientIp}:${id}`).
### Centralized Client IP Resolution (`src/lib/client-ip.ts`)
- **Untrusted Proxy Mode (Default)**:
When `TRUST_PROXY !== 'true'`, user-supplied `X-Forwarded-For`, `X-Real-IP`, or `CF-Connecting-IP` headers are **strictly ignored** to prevent IP spoofing attacks. The direct socket connection `req.ip` is used.
- *Production Behavior*: If `NODE_ENV=production`, `TRUST_PROXY !== 'true'`, and direct `req.ip` is unavailable (e.g. in self-hosted Node.js / `next start` behind a reverse proxy), the server emits a `[SECURITY CONFIGURATION WARNING]` and falls back to `'direct-client'`. For production deployments behind reverse proxies, setting `TRUST_PROXY=true` is required.
- **Trusted Proxy Mode (`TRUST_PROXY=true`)**:
When deployed behind a verified reverse proxy (e.g. Nginx, Cloudflare, AWS ALB), `TRUST_PROXY=true` must be set. The resolver:
- Enforces a maximum header length of 1024 characters (oversized headers are rejected as malformed).
- Parses multi-value proxy chains (`client, proxy1, proxy2`), extracting and validating the leftmost IP.
- Normalizes IPv4, IPv6 (including IPv4-mapped IPv6 `::ffff:192.0.2.1` and loopbacks `::1`).
- Validates IP syntax against IPv4/IPv6 standards.
### Memory Limiter vs Multi-Instance Deployments
- The built-in `SlidingWindowRateLimiter` is optimized for single-instance, serverless dev, and test environments.
- In multi-instance or horizontal cluster deployments, rate limiting must be offloaded to an edge layer (e.g., Cloudflare Rate Limiting, Nginx limit_req) or a shared distributed cache (Redis/Valkey).
---
## 3. Winner Count Contract (Zero Under-Delivery)
### Contract
Before conducting any draw, the system enforces:
$$\text{winnersCount} + \text{reserveWinnersCount} \le \text{eligibleParticipantsCount}$$
If the requested winners count plus reserve exceeds the locked snapshot's eligible participants:
- The system returns HTTP `400 VALIDATION_ERROR` (or `409 CONFLICT`).
- **The system NEVER silently clamps or reduces the winners count.**
---
## 4. Draw Concurrency & Terminal Retry Contract
### Atomic Conditional Execution
- Draw execution (`POST /api/giveaways/[id]/draw`) requires the giveaway to be in `SNAPSHOT_LOCKED` status with an existing snapshot.
- The state transition from `SNAPSHOT_LOCKED` to `DRAWN` occurs atomically inside a database transaction (`updateMany({ where: { id, status: 'SNAPSHOT_LOCKED' } })`).
- In a concurrent race (e.g. 20 or 100 simultaneous draw requests), exactly **one** request acquires the lock and transitions to `DRAWN`. All competing requests receive `409 Conflict`.
### Terminal State Replay (`DRAW_ALREADY_COMPLETED`)
- Once a giveaway is in `DRAWN` or `PUBLISHED` status, subsequent draw attempts return:
```json
{
"success": false,
"error": {
"code": "DRAW_ALREADY_COMPLETED",
"message": "Giveaway has already been drawn and finalized. Repeat draws are not permitted."
}
}
```
- Clients and UI treat `DRAW_ALREADY_COMPLETED` as a terminal final status.
---
## 5. Summary of Environment Variables
| Variable | Type | Default | Description |
|---|---|---|---|
| `NODE_ENV` | string | `development` | Environment mode (`production`, `test`, `development`) |
| `TRUST_PROXY` | boolean (`true`/`false`) | `false` | Enable only behind trusted upstream proxies |
| `ALLOW_MEMORY_IDEMPOTENCY` | boolean | `false` | Silence production warning for memory idempotency in single-instance |
| `ALLOW_MEMORY_RATE_LIMITER` | boolean | `false` | Silence production warning for memory rate limiter in single-instance |
| `USE_VK_MOCK` | boolean | `false` | Explicitly enable VkMockProvider (forbidden in production unless true) |
| `VK_SERVICE_TOKEN` | string | `undefined` | VK Application Service Access Token |