"""Client validation and utility functions.""" import re from urllib.parse import urlparse def mask_email(email: str) -> str: """ Mask email for display: user@example.com -> u***@example.com Args: email: Email address to mask Returns: Masked email string """ if '@' not in email: return email local, domain = email.split('@', 1) if len(local) <= 1: return email masked_local = local[0] + '***' return f"{masked_local}@{domain}" def normalize_client_id(client_id: str) -> str: """ Normalize client_id URL to canonical form. Rules: - Ensure https:// scheme - Remove default port (443) - Preserve path Args: client_id: Client ID URL Returns: Normalized client_id Raises: ValueError: If client_id does not use https scheme """ parsed = urlparse(client_id) # Ensure https if parsed.scheme != 'https': raise ValueError("client_id must use https scheme") # Remove default HTTPS port netloc = parsed.netloc if netloc.endswith(':443'): netloc = netloc[:-4] # Reconstruct normalized = f"https://{netloc}{parsed.path}" if parsed.query: normalized += f"?{parsed.query}" if parsed.fragment: normalized += f"#{parsed.fragment}" return normalized def validate_redirect_uri(redirect_uri: str, client_id: str) -> bool: """ Validate redirect_uri against client_id per IndieAuth spec. Rules: - Must use https scheme (except localhost) - Must share same origin as client_id OR - Must be subdomain of client_id domain OR - Can be localhost/127.0.0.1 for development Args: redirect_uri: Redirect URI to validate client_id: Client ID for comparison Returns: True if valid, False otherwise """ try: redirect_parsed = urlparse(redirect_uri) client_parsed = urlparse(client_id) # Allow localhost/127.0.0.1 for development (can use HTTP) if redirect_parsed.hostname in ('localhost', '127.0.0.1'): return True # Check scheme (must be https for non-localhost) if redirect_parsed.scheme != 'https': return False # Same origin check if (redirect_parsed.scheme == client_parsed.scheme and redirect_parsed.netloc == client_parsed.netloc): return True # Subdomain check redirect_host = redirect_parsed.hostname or '' client_host = client_parsed.hostname or '' # Must end with .{client_host} if redirect_host.endswith(f".{client_host}"): return True return False except Exception: return False def extract_domain_from_url(url: str) -> str: """ Extract domain from URL. Args: url: URL to extract domain from Returns: Domain name Raises: ValueError: If URL is invalid or has no hostname """ try: parsed = urlparse(url) if not parsed.hostname: raise ValueError("URL has no hostname") return parsed.hostname except Exception as e: raise ValueError(f"Invalid URL: {e}") from e def validate_email(email: str) -> bool: """ Validate email address format. Args: email: Email address to validate Returns: True if valid email format, False otherwise """ # Simple email validation pattern pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return bool(re.match(pattern, email))