$capabilities */ private function __construct( public readonly string $kind, public readonly int $siteId, public readonly ?int $userId, public readonly ?int $chatSessionId, private readonly array $capabilities, ) { } public static function anonymous(int $siteId): self { return new self(self::ANONYMOUS, $siteId, null, null, []); } /** @param list $capabilities */ public static function user(int $siteId, int $userId, array $capabilities, ?int $chatSessionId = null): self { if ($userId <= 0) { throw new \InvalidArgumentException('User principal requires a positive user ID.'); } return new self(self::USER, $siteId, $userId, $chatSessionId, array_values($capabilities)); } /** A visitor holding a verified chat-session token. */ public static function guest(int $siteId, int $chatSessionId): self { return new self(self::GUEST, $siteId, null, $chatSessionId, []); } /** Internal job execution; operations still go through policy. */ public static function system(int $siteId): self { return new self(self::SYSTEM, $siteId, null, null, []); } public function withChatSession(int $sessionId): self { return new self($this->kind, $this->siteId, $this->userId, $sessionId, $this->capabilities); } public function isAuthenticated(): bool { return $this->kind === self::USER; } public function can(string $capability): bool { return in_array($capability, $this->capabilities, true); } /** Stable redacted actor label for audit events. */ public function actorRef(): string { return match ($this->kind) { self::USER => 'user:' . $this->userId, self::GUEST => 'guest-session:' . $this->chatSessionId, default => $this->kind, }; } }