$verifier, 'codeChallenge' => $challenge, ]; } public function authorizationUrl(array $scopes, string $state, string $codeChallenge): string { return rtrim($this->baseUrl, '/') . '/oauth/authorize.php?' . http_build_query([ 'response_type' => 'code', 'client_id' => $this->clientId, 'redirect_uri' => $this->redirectUri, 'scope' => implode(' ', array_values(array_unique($scopes))), 'state' => $state, 'code_challenge' => $codeChallenge, 'code_challenge_method' => 'S256', ], '', '&', PHP_QUERY_RFC3986); } public function exchangeCode(string $code, string $codeVerifier): array { return $this->postForm('/oauth/token.php', [ 'grant_type' => 'authorization_code', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $code, 'redirect_uri' => $this->redirectUri, 'code_verifier' => $codeVerifier, ]); } public function refresh(string $refreshToken): array { return $this->postForm('/oauth/token.php', [ 'grant_type' => 'refresh_token', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'refresh_token' => $refreshToken, ]); } public function revoke(string $token, ?string $tokenTypeHint = null): void { $this->postForm('/oauth/revoke.php', [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'token' => $token, 'token_type_hint' => $tokenTypeHint, ], true); } private function postForm(string $path, array $fields, bool $allowEmpty = false): array { $fields = array_filter($fields, static fn ($value): bool => $value !== null && $value !== ''); $curl = curl_init(rtrim($this->baseUrl, '/') . $path); if ($curl === false) { throw new RuntimeException('No se pudo inicializar cURL.'); } curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Content-Type: application/x-www-form-urlencoded', ], CURLOPT_POSTFIELDS => http_build_query($fields, '', '&', PHP_QUERY_RFC3986), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $raw = curl_exec($curl); if ($raw === false) { $message = curl_error($curl); curl_close($curl); throw new RuntimeException('Error de red: ' . $message); } $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); if ($status < 200 || $status >= 300) { $data = json_decode($raw, true); throw new RuntimeException( (string) ($data['error_description'] ?? 'La operación OAuth falló.') ); } if ($allowEmpty && trim($raw) === '') { return []; } try { return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $exception) { throw new RuntimeException('PietroCal devolvió JSON inválido.', 0, $exception); } } }