baseUrl = $host; $this->integrationId = (string) ($params['serverusername'] ?? ''); $this->apiSecret = (string) ($params['serverpassword'] ?? ''); if ($this->baseUrl === '' || $this->integrationId === '' || $this->apiSecret === '') { throw new RuntimeException('GameCloud server hostname, integration ID and API secret are required'); } } public function health(): array { return $this->request('GET', '/api/v1/integrations/whmcs/health'); } public function upsertClient(string $externalClientId, array $payload): array { return $this->request( 'PUT', '/api/v1/integrations/whmcs/clients/' . rawurlencode($externalClientId), $payload, 'client:' . $externalClientId ); } public function provisionService(array $payload): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services', $payload, 'service:provision:' . $payload['externalServiceId'] ); } public function suspendService(string $externalServiceId, array $payload = []): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/suspend', $payload, 'service:suspend:' . $externalServiceId ); } public function unsuspendService(string $externalServiceId): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/unsuspend', [], 'service:unsuspend:' . $externalServiceId ); } public function terminateService(string $externalServiceId): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/terminate', [], 'service:terminate:' . $externalServiceId ); } public function changePackage(string $externalServiceId, array $payload): array { return $this->request( 'PATCH', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/change-package', $payload, 'service:change-package:' . $externalServiceId ); } private function request(string $method, string $path, array $body = [], ?string $idempotencySuffix = null): array { $bodyJson = $body === [] ? '' : json_encode($body, JSON_THROW_ON_ERROR); $timestamp = (string) (int) (microtime(true) * 1000); $nonce = bin2hex(random_bytes(16)); $idempotencyKey = $idempotencySuffix ?: $method . ':' . $path . ':' . $timestamp; $signatureBase = implode("\n", [ strtoupper($method), $path, '', hash('sha256', $bodyJson), $timestamp, $nonce, $idempotencyKey, ]); $signature = hash_hmac('sha256', $signatureBase, $this->apiSecret); $ch = curl_init($this->baseUrl . $path); if ($ch === false) { throw new RuntimeException('Unable to initialize HTTP client'); } curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', 'X-HGC-Integration-ID: ' . $this->integrationId, 'X-HGC-Timestamp: ' . $timestamp, 'X-HGC-Nonce: ' . $nonce, 'X-HGC-Idempotency-Key: ' . $idempotencyKey, 'X-HGC-Signature: ' . $signature, ], CURLOPT_POSTFIELDS => $bodyJson, ]); $responseBody = curl_exec($ch); $statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($responseBody === false) { throw new RuntimeException('GameCloud request failed: ' . $curlError); } $decoded = json_decode($responseBody, true); if ($statusCode >= 400) { $message = is_array($decoded) ? ($decoded['detail'] ?? $decoded['title'] ?? $responseBody) : $responseBody; throw new RuntimeException('GameCloud API error (' . $statusCode . '): ' . $message); } return is_array($decoded) ? $decoded : []; } }