"""AuditEvent — append-only security & business audit trail. Audit events are never mutated or deleted. They record WHO did WHAT to WHICH entity WHEN and from WHERE. """ from __future__ import annotations from typing import Any, Optional, Dict from pydantic import Field from asf_domain.base import ( ASFBaseModel, TenantScopedModel, ASFFieldID, UTCDatetime, AuditAction, generate_id, ) class AuditEventBase(ASFBaseModel): action: AuditAction = Field(..., description="What happened.") resource_type: str = Field( ..., max_length=80, description="Affected entity kind (organization, project, artifact, job, api_key, …).", ) resource_id: Optional[ASFFieldID] = Field(None, description="Affected entity id if applicable.") actor_id: Optional[ASFFieldID] = Field( None, description="User id that caused the event, or null for system actions." ) actor_type: str = Field( "user", max_length=40, description="user | system | api_key | service — kind of actor that caused the event.", ) success: bool = True reason: Optional[str] = Field( None, max_length=500, description="For failed events, why the action was rejected." ) class AuditEventCreate(AuditEventBase): project_id: Optional[ASFFieldID] = None ip_address: Optional[str] = Field(None, max_length=64) user_agent: Optional[str] = Field(None, max_length=512) correlation_id: Optional[str] = Field(None, max_length=128) metadata: Dict[str, Any] = Field( default_factory=dict, description="Structured detail of the change. NEVER log secrets here.", ) class AuditEvent(TenantScopedModel, AuditEventBase): id: ASFFieldID = Field(default_factory=generate_id) project_id: Optional[ASFFieldID] = None ip_address: Optional[str] = None user_agent: Optional[str] = None correlation_id: Optional[str] = None metadata: Dict[str, Any] = Field(default_factory=dict) created_at: UTCDatetime = ...