"""Author RAG Chatbot SaaS — Anonymous Visitor Fingerprinting. Generates a stable, anonymous fingerprint for each visitor session. Used for unique-visitor counting in analytics — NOT for tracking across sites. Privacy rules: - No PII ever stored or logged - Raw IP is hashed immediately and never persisted - Fingerprint is a one-way SHA-256 hash — not reversible to IP - Fingerprint TTL is session-scoped (30 minutes inactivity = new fingerprint) RULE: This module must NEVER store or return a raw IP address. """ import hashlib import structlog logger = structlog.get_logger(__name__) def generate_visitor_fingerprint( ip_address: str, user_agent: str, accept_language: str = "", accept_encoding: str = "", ) -> str: """Generate a stable, anonymous visitor fingerprint. Combines multiple non-PII signals into a 32-char hex fingerprint. The raw IP is hashed immediately — the fingerprint itself is not reversible to any identifying information. Args: ip_address: Client IP address (used only in hash, never stored). user_agent: Browser User-Agent string. accept_language: Accept-Language header value. accept_encoding: Accept-Encoding header value. Returns: 32-character hex fingerprint string. """ components = [ ip_address or "unknown", user_agent or "", accept_language or "", accept_encoding or "", ] combined = "|".join(components) fingerprint = hashlib.sha256(combined.encode("utf-8")).hexdigest()[:32] logger.debug("Visitor fingerprint generated", fingerprint_prefix=fingerprint[:8]) return fingerprint def fingerprint_from_request(request) -> str: """Convenience wrapper: extract fingerprint signals from a FastAPI Request. Args: request: FastAPI / Starlette Request object. Returns: 32-character hex fingerprint string. """ ip = request.client.host if request.client else "unknown" forwarded_for = request.headers.get("X-Forwarded-For", "") if forwarded_for: # Use the leftmost IP (original client) when behind a proxy ip = forwarded_for.split(",")[0].strip() return generate_visitor_fingerprint( ip_address=ip, user_agent=request.headers.get("User-Agent", ""), accept_language=request.headers.get("Accept-Language", ""), accept_encoding=request.headers.get("Accept-Encoding", ""), )