Files
smueller 316679a913
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 8s
CI / Go — edge-gateway build (push) Successful in 18s
Enhance WHMCS integration with mTLS support and product mapping features. Added mTLS configuration options, updated API endpoints for mTLS status and fingerprint registration, and implemented product validation API. Updated database schema and documentation accordingly.
2026-06-30 13:17:12 +02:00

181 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
final class HexaGameCloudAddonApiClient
{
private array $mtls = [];
public function __construct(
private readonly string $baseUrl,
private readonly string $integrationId,
private readonly string $apiSecret,
array $mtls = [],
) {
if ($this->baseUrl === '' || $this->integrationId === '' || $this->apiSecret === '') {
throw new RuntimeException('API URL, integration ID and secret are required');
}
$this->mtls = $mtls;
}
public function health(): array
{
return $this->request('GET', '/api/v1/integrations/whmcs/health');
}
public function dashboardStats(): array
{
return $this->request('GET', '/api/v1/integrations/whmcs/dashboard/stats');
}
public function listAccounts(): array
{
return $this->request('GET', '/api/v1/integrations/whmcs/accounts');
}
public function listEvents(?string $cursor = null, int $limit = 50): array
{
$query = $cursor
? ('?cursor=' . rawurlencode($cursor) . '&limit=' . $limit)
: ('?limit=' . $limit);
return $this->request('GET', '/api/v1/integrations/whmcs/events' . $query);
}
public function acknowledgeEvents(array $eventIds, bool $deadLetter = false, ?string $reason = null): array
{
$payload = ['eventIds' => $eventIds, 'deadLetter' => $deadLetter];
if ($reason !== null) {
$payload['deadLetterReason'] = $reason;
}
return $this->request('POST', '/api/v1/integrations/whmcs/events/ack', $payload);
}
public function reconcileAll(array $payload = []): array
{
return $this->request(
'POST',
'/api/v1/integrations/whmcs/reconcile',
$payload,
'reconcile:all:' . date('Y-m-d-His')
);
}
public function listPlans(): array
{
return $this->request('GET', '/api/v1/integrations/whmcs/catalog/plans');
}
public function validatePlan(string $planSlug): array
{
return $this->request('POST', '/api/v1/integrations/whmcs/catalog/validate-plan', [
'planSlug' => $planSlug,
]);
}
public function registerMtlsFingerprint(string $fingerprint, ?string $subject = null): array
{
return $this->request(
'PUT',
'/api/v1/integrations/whmcs/mtls/fingerprint',
array_filter([
'fingerprint' => $fingerprint,
'subject' => $subject,
]),
'mtls:register:' . date('Y-m-d')
);
}
public function mtlsStatus(): array
{
return $this->request('GET', '/api/v1/integrations/whmcs/mtls/status');
}
private function request(string $method, string $pathWithQuery, array $body = [], ?string $idempotencySuffix = null): array
{
$pathParts = explode('?', $pathWithQuery, 2);
$path = $pathParts[0];
$canonicalQuery = '';
if (isset($pathParts[1]) && $pathParts[1] !== '') {
parse_str($pathParts[1], $queryParams);
ksort($queryParams);
$pairs = [];
foreach ($queryParams as $key => $value) {
$pairs[] = rawurlencode((string) $key) . '=' . rawurlencode((string) $value);
}
$canonicalQuery = implode('&', $pairs);
}
$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,
$canonicalQuery,
hash('sha256', $bodyJson),
$timestamp,
$nonce,
$idempotencyKey,
]);
$signature = hash_hmac('sha256', $signatureBase, $this->apiSecret);
$requestUrl = rtrim($this->baseUrl, '/') . $path . ($canonicalQuery !== '' ? ('?' . $canonicalQuery) : '');
$headers = [
'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,
];
if ($this->mtls !== [] && class_exists('HexaGameCloudMtlsConfig')) {
$headers = array_merge($headers, HexaGameCloudMtlsConfig::fingerprintHeader($this->mtls));
}
$ch = curl_init($requestUrl);
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 => $headers,
CURLOPT_POSTFIELDS => $bodyJson,
]);
if ($this->mtls !== [] && class_exists('HexaGameCloudMtlsConfig')) {
HexaGameCloudMtlsConfig::applyToCurl($ch, $this->mtls);
}
$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 : [];
}
}