header('Paddle-Signature'); if ($secret === '' || $header === '') { throw new RuntimeException('Paddle webhook verification is not configured.'); } $parts = []; foreach (explode(';', $header) as $part) { [$key, $value] = array_pad( explode('=', trim($part), 2), 2, null ); if ($key && $value) { $parts[$key][] = $value; } } $timestamp = isset($parts['ts'][0]) ? (int) $parts['ts'][0] : 0; $signatures = $parts['h1'] ?? []; $tolerance = max( 1, (int) config('rsync-studio.paddle.timestamp_tolerance', 300) ); if (!$timestamp || abs(time() - $timestamp) > $tolerance) { throw new RuntimeException( 'Paddle webhook timestamp is outside the allowed tolerance.' ); } $signedPayload = $timestamp . ':' . $rawBody; $expected = hash_hmac( 'sha256', $signedPayload, $secret ); Log::info('Paddle webhook signature debug', [ 'header' => $header, 'timestamp' => $timestamp, 'server_timestamp' => time(), 'body_length' => strlen($rawBody), 'body_sha256' => hash('sha256', $rawBody), 'expected_h1' => $expected, 'received_h1' => $signatures, ]); foreach ($signatures as $signature) { if (hash_equals($expected, $signature)) { return; } } throw new RuntimeException('Paddle webhook signature is invalid.'); } }