json(['error' => 'Webhook not configured'], 500); } $signature = trim((string) $request->header('X-Signature', '')); if ($signature === '') { return response()->json(['error' => 'Signature missing'], 400); } $payload = $request->getContent(); if (! $this->signatureMatches($payload, $signature, $secret)) { return response()->json(['error' => 'Invalid signature'], 400); } $decoded = json_decode($payload, true); if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decoded)) { Log::warning('RevenueCat webhook received invalid JSON', [ 'error' => json_last_error_msg(), ]); return response()->json(['error' => 'Invalid payload'], 400); } ProcessRevenueCatWebhook::dispatch( $decoded, (string) $request->header('X-Event-Id', '') ); return response()->json(['status' => 'accepted'], 202); } private function signatureMatches(string $payload, string $providedSignature, string $secret): bool { $normalized = preg_replace('/\s+/', '', $providedSignature); if (! is_string($normalized)) { return false; } $candidates = [ base64_encode(hash_hmac('sha1', $payload, $secret, true)), base64_encode(hash_hmac('sha256', $payload, $secret, true)), hash_hmac('sha1', $payload, $secret), hash_hmac('sha256', $payload, $secret), ]; foreach ($candidates as $expected) { if (hash_equals($expected, $normalized)) { return true; } } return false; } }