base=rtrim($base,'/'); } public function request(string $method,string $path,?array $body=null,?string $idempotencyKey=null,?string $ifMatch=null): array { if (!str_starts_with($path,'/') || str_starts_with($path,'//') || str_contains($path,'..') || str_contains($path,'://')) throw new InvalidArgumentException('Relative API path required'); if ($method!=='GET' && !$idempotencyKey) throw new InvalidArgumentException('Persist an idempotency key before mutations'); foreach ([$idempotencyKey,$ifMatch] as $value) if ($value!==null && preg_match('/[\r\n]/',$value)) throw new InvalidArgumentException('Invalid header'); $headers=['Authorization: Bearer '.$this->key,'Accept: application/json']; if ($body!==null) $headers[]='Content-Type: application/json'; if ($idempotencyKey) $headers[]='Idempotency-Key: '.$idempotencyKey; if ($ifMatch) $headers[]='If-Match: '.$ifMatch; $responseHeaders=[]; $c=curl_init($this->base.$path); curl_setopt_array($c,[CURLOPT_CUSTOMREQUEST=>$method,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>10,CURLOPT_FOLLOWLOCATION=>false,CURLOPT_HTTPHEADER=>$headers,CURLOPT_HEADERFUNCTION=>static function($curl,$line) use (&$responseHeaders) { $parts=explode(':',$line,2);if(count($parts)===2)$responseHeaders[strtolower(trim($parts[0]))]=trim($parts[1]);return strlen($line); }]); if ($body!==null) curl_setopt($c,CURLOPT_POSTFIELDS,json_encode($body,JSON_THROW_ON_ERROR)); $raw=curl_exec($c);$status=curl_getinfo($c,CURLINFO_HTTP_CODE);curl_close($c); if ($raw===false) throw new RuntimeException('Transport outcome unconfirmed. Retain the same request identity.'); try { $result=json_decode($raw,true,512,JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new WingsFlowApiException($status,'invalid_response',$responseHeaders['x-request-id']??null,$status>=500,$responseHeaders['retry-after']??null); } if (!is_array($result)) throw new WingsFlowApiException($status,'invalid_response',$responseHeaders['x-request-id']??null,false,null); if ($status<200 || $status>=300) throw new WingsFlowApiException($status,$result['error']['code']??null,$responseHeaders['x-request-id']??null,($result['error']['retryable']??false)===true,$responseHeaders['retry-after']??null); return $result; } }