randomayzer/docs/PRODUCTION_GUARDS.md

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

  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:
    {
      "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