settings ??= (array) Config::get('iam', []); if (!self::enabled($this->settings)) { throw new RuntimeException('FEATURE_DISABLED'); } self::https((string) ($this->settings['api_url'] ?? '')); foreach (['public_url', 'erp_url'] as $field) { self::origin((string) ($this->settings[$field] ?? '')); } if (strlen((string) ($this->settings['v2_token'] ?? '')) < 32) { throw new RuntimeException('SERVICE_UNAVAILABLE'); } } public static function enabled(?array $settings = null): bool { $settings ??= (array) Config::get('iam', []); return filter_var($settings['v2_enabled'] ?? false, FILTER_VALIDATE_BOOL); } public static function feature(string $feature, ?array $settings = null): bool { $settings ??= (array) Config::get('iam', []); if (!in_array($feature, ['management', 'launch'], true)) { return false; } return self::enabled($settings) && filter_var($settings['v2_' . $feature . '_enabled'] ?? false, FILTER_VALIDATE_BOOL); } public static function https(string $url): string { $p = parse_url($url); if (!$p || ($p['scheme'] ?? '') !== 'https' || empty($p['host']) || isset($p['user']) || isset($p['pass']) || isset($p['fragment']) || isset($p['query']) || preg_match('/[\x00-\x20\x7f\\\\]/', $url)) { throw new RuntimeException('INVALID_FIXED_URL'); } return rtrim($url, '/'); } public static function origin(string $url): string { $url = self::https($url); if ((parse_url($url, PHP_URL_PATH) ?? '') !== '') { throw new RuntimeException('INVALID_FIXED_URL'); } return $url; } public function call(string $method, string $path, array $body = [], ?string $key = null): array { if (!preg_match('#^/[a-zA-Z0-9/_?=&%.:-]+$#D', $path) || str_contains($path, '..')) { throw new RuntimeException('INVALID_PATH'); } $url = rtrim($this->settings['api_url'], '/') . '/internal/v2/applications/' . rawurlencode($this->settings['application_id'] ?? 'zyt') . $path; $management = preg_match('#^/(identities|operations|audit|batch|jobs)(/|[?]|$)#D', $path) === 1; $credential = (string) ($this->settings[$management ? 'v2_management_token' : 'v2_token'] ?? ''); if (strlen($credential) < 32 || preg_match('/[\r\n]/', $credential) || ($management && hash_equals((string) $this->settings['v2_token'], $credential))) { throw new RuntimeException('SERVICE_UNAVAILABLE'); } $headers = ['Accept: application/json', 'Content-Type: application/json', 'Authorization: Bearer ' . $credential]; if ($method !== 'GET') { $headers[] = 'X-Request-ID: ' . bin2hex(random_bytes(16)); $headers[] = 'Idempotency-Key: ' . ($key ?? bin2hex(random_bytes(24))); } $data = $method === 'GET' ? '' : json_encode($body, JSON_THROW_ON_ERROR); if ($this->transport) { $r = ($this->transport)($method, $url, $headers, $data); } else { $c = curl_init($url); $response = ''; curl_setopt_array($c, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => $data, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 10, CURLOPT_WRITEFUNCTION => static function ($c, $chunk) use (&$response) { if (strlen($response) + strlen($chunk) > 2097152) { return 0; } $response .= $chunk; return strlen($chunk); }]); $ok = curl_exec($c); $r = ['status' => curl_getinfo($c, CURLINFO_RESPONSE_CODE), 'body' => $ok === false ? '' : $response]; curl_close($c); } $result = json_decode($r['body'] ?? '', true); if (!is_array($result)) { throw new RuntimeException('SERVICE_UNAVAILABLE'); } $operationEnvelope = isset($result['id']) && is_string($result['id']) && in_array($result['status'] ?? '', ['pending', 'succeeded', 'failed', 'conflict'], true) && preg_match('#^/(operations|jobs|batch)(/|$)#D', $path); if (($r['status'] ?? 0) < 200 || $r['status'] >= 300 || (isset($result['error']) && !$operationEnvelope)) { $code = $result['error']['code'] ?? 'SERVICE_UNAVAILABLE'; throw new RuntimeException(preg_match('/^[A-Z_]{3,64}$/D', $code) ? $code : 'SERVICE_UNAVAILABLE'); } return $result; } }