curlErrno; } } class edge_broker_http_exception extends Exception { public function __construct(string $message, private readonly int $statusCode, int $code = 0, ?Exception $previous = null) { parent::__construct($message, $code, $previous); } public function statusCode(): int { return $this->statusCode; } } class edge_broker_client { private const DEFAULT_BROKER_URL = 'http://edge-broker:4300'; public function __construct( private readonly ?string $baseUrl = null, private readonly ?string $sharedSecret = null, private readonly int $timeoutSeconds = 10 ) { } public function isConfigured(): bool { return trim((string)$this->resolveBaseUrl()) !== ''; } public function dispatchCommand(int $gatewayId, string $commandType, array $payload): array { $url = rtrim($this->resolveBaseUrl(), '/') . '/api/gateways/' . $gatewayId . '/commands'; $response = $this->request('POST', $url, [ 'commandType' => $commandType, 'payload' => $payload, ]); return is_array($response) ? $response : ['ok' => false, 'response' => $response]; } public function validateAgent(int $gatewayId, string $agentToken): array { $url = rtrim($this->resolveBaseUrl(), '/') . '/api/internal/agent/auth'; $response = $this->request('POST', $url, [ 'gatewayId' => $gatewayId, 'agentToken' => $agentToken, ]); return is_array($response) ? $response : []; } public function validateShellSession(string $sessionToken): array { $url = rtrim($this->resolveBaseUrl(), '/') . '/api/internal/shell/auth'; $response = $this->request('POST', $url, [ 'sessionToken' => $sessionToken, ]); return is_array($response) ? $response : []; } public function closeShellSession(int $sessionId, string $sessionToken, string $transcript, string $closedReason): array { $url = rtrim($this->resolveBaseUrl(), '/') . '/api/internal/shell-sessions/' . $sessionId . '/close'; $response = $this->request('POST', $url, [ 'sessionToken' => $sessionToken, 'transcript' => $transcript, 'closedReason' => $closedReason, ]); return is_array($response) ? $response : []; } private function resolveBaseUrl(): string { return trim((string)($this->baseUrl ?? getenv('EDGE_BROKER_URL') ?: self::DEFAULT_BROKER_URL)); } private function resolveSharedSecret(): string { return trim((string)($this->sharedSecret ?? getenv('EDGE_BROKER_SHARED_SECRET') ?: getenv('EDGE_INTERNAL_SECRET') ?: '')); } /** * @throws Exception */ private function request(string $method, string $url, array $payload): array|object|null { if (trim($url) === '') { throw new Exception('Edge broker URL is not configured'); } $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeoutSeconds); curl_setopt($ch, CURLOPT_HTTPHEADER, array_values(array_filter([ 'Content-Type: application/json', $this->resolveSharedSecret() !== '' ? 'X-Edge-Broker-Secret: ' . $this->resolveSharedSecret() : null, ]))); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE)); $rawResponse = curl_exec($ch); $statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErrno = curl_errno($ch); $curlError = curl_error($ch); curl_close($ch); if ($rawResponse === false) { throw new edge_broker_transport_exception('Edge broker request failed: ' . $curlError, $curlErrno); } $decoded = json_decode((string)$rawResponse, true); if ($statusCode >= 400) { $message = is_array($decoded) ? (string)($decoded['error'] ?? $decoded['message'] ?? 'Edge broker request failed') : 'Edge broker request failed'; throw new edge_broker_http_exception($message, $statusCode); } return $decoded; } }