6.2 KiB
6.2 KiB
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
- Scoped Storage Key:
Keys are composite and scoped by
operation,giveawayId, andidempotencyKey:format: ${operation}:${giveawayId || 'global'}:${key}This prevents cross-endpoint and cross-giveaway key collision. - Request Fingerprinting:
Every request computes a canonical SHA-256 fingerprint:
requestFingerprint = SHA256(canonicalStringify(requestPayload)) - 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 Conflictwith error codeIDEMPOTENCY_KEY_REUSED.
- 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.
- Maximum key length is 128 characters (exceeding length returns
2. Rate Limiting & Client Identity Resolution
Client Identity Scoping Architecture
- Authenticated Routes (
/api/giveaways*): Rate limits are keyed strictly by trusted server-sidesessionUser.id(e.g.draw-execute:${sessionUser.id}:${id},giveaways-list:${sessionUser.id}) after session authentication and ownership checks. This ensures:- Different organizers have isolated rate limit buckets and never block each other, even when
req.ipis unpopulated. - Unauthenticated attackers receive
401 Unauthorizedbefore reaching the rate limiter and cannot drain any organizer's quota.
- Different organizers have isolated rate limit buckets and never block each other, even when
- Anonymous / Hybrid Routes (
/api/posts/preview,/api/auth/vk/start,/api/giveaways/[id]/verify):POST /api/posts/preview: Usespost-preview:user:${sessionUser.id}if a valid session exists, andpost-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-suppliedX-Forwarded-For,X-Real-IP, orCF-Connecting-IPheaders are strictly ignored to prevent IP spoofing attacks. The direct socket connectionreq.ipis used.- Production Behavior: If
NODE_ENV=production,TRUST_PROXY !== 'true', and directreq.ipis unavailable (e.g. in self-hosted Node.js /next startbehind a reverse proxy), the server emits a[SECURITY CONFIGURATION WARNING]and falls back to'direct-client'. For production deployments behind reverse proxies, settingTRUST_PROXY=trueis required.
- Production Behavior: If
- Trusted Proxy Mode (
TRUST_PROXY=true): When deployed behind a verified reverse proxy (e.g. Nginx, Cloudflare, AWS ALB),TRUST_PROXY=truemust 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.1and loopbacks::1). - Validates IP syntax against IPv4/IPv6 standards.
Memory Limiter vs Multi-Instance Deployments
- The built-in
SlidingWindowRateLimiteris 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(or409 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 inSNAPSHOT_LOCKEDstatus with an existing snapshot. - The state transition from
SNAPSHOT_LOCKEDtoDRAWNoccurs 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 receive409 Conflict.
Terminal State Replay (DRAW_ALREADY_COMPLETED)
- Once a giveaway is in
DRAWNorPUBLISHEDstatus, subsequent draw attempts return:{ "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_COMPLETEDas 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 |