delivery->endpoint; // El endpoint pudo haberse borrado o desactivado entre que se // encoló el job y que se procesó — no tiene sentido reintentar. if (! $endpoint || ! $endpoint->active) { return; } // Re-chequeo anti-SSRF: la URL ya se validó al crear/editar el // webhook, pero un dominio público en ese momento puede resolver a // una IP interna más tarde (DNS rebinding). No reintentamos esto — // si sigue sin ser seguro en el próximo intento, va a fallar igual. if (! WebhookEndpoints::isSafeUrl($endpoint->url)) { $this->delivery->update([ 'response_body' => 'Bloqueado: la URL resuelve a una dirección privada/reservada.', ]); return; } $body = json_encode([ 'event' => $this->delivery->event, 'data' => $this->delivery->payload, ]); $signature = hash_hmac('sha256', $body, $endpoint->secret); $this->delivery->increment('attempts'); try { $response = Http::timeout(5) ->withHeaders([ 'Content-Type' => 'application/json', 'X-Webhook-Event' => $this->delivery->event, 'X-Webhook-Signature' => 'sha256='.$signature, ]) ->withBody($body, 'application/json') ->post($endpoint->url); $this->delivery->update([ 'response_status' => $response->status(), 'response_body' => Str::limit($response->body(), 1000), 'delivered_at' => $response->successful() ? now() : null, ]); if (! $response->successful()) { throw new \RuntimeException("El endpoint del webhook respondió {$response->status()}."); } } catch (\Throwable $e) { $this->delivery->update([ 'response_body' => Str::limit($e->getMessage(), 1000), ]); throw $e; } } }