defmodule BerrypodWeb.Plugs.VerifyPrintifyWebhook do @moduledoc """ Verifies Printify webhook signatures using HMAC-SHA256. Expects: - Raw body cached in conn.assigns[:raw_body] - X-Pfy-Signature header in format "sha256={hex_digest}" - Webhook secret stored in provider connection config """ import Plug.Conn require Logger alias Berrypod.Products def init(opts), do: opts def call(conn, _opts) do with {:ok, signature} <- get_signature(conn), {:ok, secret} <- get_webhook_secret(), :ok <- verify_signature(conn.assigns[:raw_body], secret, signature) do conn else {:error, reason} -> Logger.warning("Printify webhook verification failed: #{reason}") conn |> put_resp_content_type("application/json") |> send_resp(401, Jason.encode!(%{error: "Invalid signature"})) |> halt() end end defp get_signature(conn) do case get_req_header(conn, "x-pfy-signature") do ["sha256=" <> hex_digest] -> {:ok, hex_digest} [_other] -> {:error, :invalid_signature_format} [] -> {:error, :missing_signature} end end defp get_webhook_secret do case Products.get_provider_connection_by_type("printify") do %{config: %{"webhook_secret" => secret}} when is_binary(secret) and secret != "" -> {:ok, secret} _ -> {:error, :no_webhook_secret} end end defp verify_signature(body, secret, expected_hex) do computed = :crypto.mac(:hmac, :sha256, secret, body || "") |> Base.encode16(case: :lower) if Plug.Crypto.secure_compare(computed, String.downcase(expected_hex)) do :ok else {:error, :signature_mismatch} end end end