# `LatticeStripe.Webhook`
[🔗](https://github.com/szTheory/lattice_stripe/blob/v0.2.0/lib/lattice_stripe/webhook.ex#L1)

Stripe webhook signature verification and event construction.

LatticeStripe.Webhook provides pure-functional HMAC-SHA256 signature verification
for incoming Stripe webhook payloads. It is designed to be used in a Plug pipeline
or any web framework — it has no Plug dependency itself.

## Usage

    # In a Plug or controller action, after reading the raw body:
    raw_body = conn.assigns[:raw_body]
    sig_header = Plug.Conn.get_req_header(conn, "stripe-signature") |> List.first()
    secret = Application.fetch_env!(:my_app, :stripe_webhook_secret)

    case LatticeStripe.Webhook.construct_event(raw_body, sig_header, secret) do
      {:ok, event} ->
        handle_event(event)
        send_resp(conn, 200, "ok")

      {:error, :missing_header} ->
        send_resp(conn, 400, "Missing Stripe-Signature header")

      {:error, :timestamp_expired} ->
        send_resp(conn, 400, "Webhook timestamp too old")

      {:error, reason} ->
        send_resp(conn, 400, "Signature verification failed: #{reason}")
    end

## Important: Raw Body Requirement

Stripe signs the **raw, unmodified request body**. Most web frameworks parse
the body and discard the original bytes. You must configure your framework to
preserve the raw body before calling these functions. See the LatticeStripe
Plug documentation for a ready-made solution.

## Replay Attack Protection

By default, `verify_signature/3` rejects webhooks with a timestamp older than
300 seconds (5 minutes). Override with `tolerance: seconds` in opts.

## Multiple Secrets (Secret Rotation)

Pass a list of secrets to verify against any of them. Useful during Stripe
webhook secret rotation — the new and old secret both work until rotation completes.

    Webhook.verify_signature(payload, header, [old_secret, new_secret])

## Stripe API Reference

See the [Stripe Webhooks documentation](https://docs.stripe.com/webhooks) for the full
webhook reference, event catalog, and delivery guarantees.

# `secret`

```elixir
@type secret() :: String.t() | [String.t(), ...]
```

# `verify_error`

```elixir
@type verify_error() ::
  :missing_header
  | :invalid_header
  | :no_matching_signature
  | :timestamp_expired
```

# `construct_event`

```elixir
@spec construct_event(String.t(), String.t() | nil, secret(), keyword()) ::
  {:ok, LatticeStripe.Event.t()} | {:error, verify_error()}
```

Verifies a Stripe webhook signature and, if valid, constructs a typed `%Event{}`.

This is the primary function for handling incoming webhooks. It:
1. Verifies the `Stripe-Signature` header using HMAC-SHA256
2. Checks the timestamp is within the tolerance window (replay attack protection)
3. Decodes the JSON payload into a `%LatticeStripe.Event{}` struct

## Parameters

- `payload` - The raw, unmodified request body string
- `sig_header` - The value of the `Stripe-Signature` header (e.g., `"t=1234,v1=abc..."`)
- `secret` - Your webhook signing secret (string or list of strings for rotation)
- `opts` - Options:
  - `:tolerance` - max age in seconds (default: 300). Set `0` to disable staleness check.

## Returns

- `{:ok, %Event{}}` on success
- `{:error, verify_error()}` on failure — see `t:verify_error/0`

# `construct_event!`

```elixir
@spec construct_event!(String.t(), String.t() | nil, secret(), keyword()) ::
  LatticeStripe.Event.t()
```

Like `construct_event/4` but raises `SignatureVerificationError` on failure.

## Returns

- `%Event{}` on success
- Raises `LatticeStripe.Webhook.SignatureVerificationError` on failure

# `generate_test_signature`

```elixir
@spec generate_test_signature(String.t(), String.t(), keyword()) :: String.t()
```

Generates a Stripe-compatible webhook signature header for testing.

Use this in tests to produce a `Stripe-Signature` header that passes
`verify_signature/3`. This avoids hard-coding computed HMAC values in tests
and correctly simulates what Stripe's servers send.

## Parameters

- `payload` - The JSON-encoded payload string
- `secret` - The webhook signing secret
- `opts` - Options:
  - `:timestamp` - Unix timestamp integer to embed (default: current time)

## Returns

A `Stripe-Signature` header value string, e.g. `"t=1680000000,v1=abc123..."`.

## Example

    header = LatticeStripe.Webhook.generate_test_signature(payload, secret)
    {:ok, event} = LatticeStripe.Webhook.construct_event(payload, header, secret)

# `verify_signature`

```elixir
@spec verify_signature(String.t(), String.t() | nil, secret(), keyword()) ::
  {:ok, integer()} | {:error, verify_error()}
```

Verifies a Stripe webhook signature header against a payload and secret.

Performs timing-safe HMAC-SHA256 comparison via `Plug.Crypto.secure_compare/2`.
Returns the parsed timestamp integer on success (useful for logging).

## Parameters

- `payload` - The raw request body string
- `sig_header` - The `Stripe-Signature` header value
- `secret` - Signing secret or list of secrets (for rotation)
- `opts` - Options:
  - `:tolerance` - max timestamp age in seconds (default: 300)

## Returns

- `{:ok, timestamp}` where `timestamp` is a Unix integer on success
- `{:error, :missing_header}` — no header provided
- `{:error, :invalid_header}` — header is present but malformed
- `{:error, :timestamp_expired}` — timestamp older than tolerance
- `{:error, :no_matching_signature}` — HMAC doesn't match any provided secret

# `verify_signature!`

```elixir
@spec verify_signature!(String.t(), String.t() | nil, secret(), keyword()) ::
  integer()
```

Like `verify_signature/4` but raises `SignatureVerificationError` on failure.

## Returns

- `timestamp` (integer) on success
- Raises `LatticeStripe.Webhook.SignatureVerificationError` on failure

---

*Consult [api-reference.md](api-reference.md) for complete listing*
