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

Phoenix Plug for Stripe webhook signature verification and event dispatch.

`LatticeStripe.Webhook.Plug` verifies the `Stripe-Signature` header on incoming
webhook requests, constructs a typed `%LatticeStripe.Event{}`, and either assigns
it to the connection (pass-through mode) or dispatches it to your handler module.

## Mounting Strategies

### Option A: Endpoint-level with `at:` path matching

Mount the plug in `endpoint.ex` before `Plug.Parsers`, using the `at:` option
to restrict it to a specific path. The plug intercepts matching requests and
passes everything else through.

    # endpoint.ex
    plug LatticeStripe.Webhook.Plug,
      at: "/webhooks/stripe",
      secret: System.fetch_env!("STRIPE_WEBHOOK_SECRET"),
      handler: MyApp.StripeHandler

    plug Plug.Parsers,
      parsers: [:json],
      pass: ["application/json"],
      json_decoder: Jason

### Option B: Router-level via `forward` (with CacheBodyReader)

Mount after `Plug.Parsers` using `Plug.Parsers` + `CacheBodyReader` so the raw
body is preserved. Then forward in your router:

    # endpoint.ex
    plug Plug.Parsers,
      parsers: [:urlencoded, :multipart, :json],
      pass: ["*/*"],
      json_decoder: Jason,
      body_reader: {LatticeStripe.Webhook.CacheBodyReader, :read_body, []}

    # router.ex
    forward "/webhooks/stripe", LatticeStripe.Webhook.Plug,
      secret: System.fetch_env!("STRIPE_WEBHOOK_SECRET"),
      handler: MyApp.StripeHandler

## Operation Modes

### Handler mode (recommended)

When a `:handler` module is configured, the plug dispatches the verified event
to `handler.handle_event/1` and sends the HTTP response:

- Handler returns `:ok` or `{:ok, _}` → responds `200 ""`
- Handler returns `:error` or `{:error, _}` → responds `400 ""`
- Handler raises → exception propagates (not caught)
- Handler returns anything else → raises `RuntimeError`

    plug LatticeStripe.Webhook.Plug,
      secret: "whsec_...",
      handler: MyApp.StripeHandler

### Pass-through mode

Without a `:handler`, the plug assigns the verified event to
`conn.assigns.stripe_event` and passes the connection to the next plug or
controller. Your controller reads the event and sends its own response.

    plug LatticeStripe.Webhook.Plug,
      secret: "whsec_...",
      at: "/webhooks/stripe"

    # In your controller:
    def webhook(conn, _params) do
      event = conn.assigns.stripe_event
      handle_event(event)
      send_resp(conn, 200, "ok")
    end

## Raw Body Requirement

Stripe signs the **raw, unmodified request body**. Most frameworks parse the
body and discard the original bytes. Two solutions:

1. **Mount before `Plug.Parsers`** using `at:` — the plug reads the body
   directly before parsers consume it.
2. **Use `CacheBodyReader`** — configure `Plug.Parsers` to cache the raw bytes
   in `conn.private[:raw_body]`. See `LatticeStripe.Webhook.CacheBodyReader`.

## Secret Resolution

The `:secret` option supports runtime resolution to avoid compile-time secrets:

    # Static string (simple)
    secret: "whsec_..."

    # List of strings (secret rotation — any match succeeds)
    secret: ["whsec_old...", "whsec_new..."]

    # MFA tuple (resolved at call time)
    secret: {MyApp.Config, :stripe_webhook_secret, []}

    # Zero-arity function (resolved at call time)
    secret: fn -> System.fetch_env!("STRIPE_WEBHOOK_SECRET") end

## Configuration Options

- `:secret` (required) — Webhook signing secret. See "Secret Resolution" above.
- `:handler` — Module implementing `LatticeStripe.Webhook.Handler`. If omitted,
  runs in pass-through mode.
- `:at` — Mount path (e.g., `"/webhooks/stripe"`). When set, the plug only
  processes requests matching this path; other paths pass through. Non-POST
  requests to this path return `405 Method Not Allowed`.
- `:tolerance` — Maximum age of the webhook timestamp in seconds (default: 300).

# `call`

Processes the connection through webhook verification.

Routes based on HTTP method and path (when `at:` is configured):

- POST to matching path (or any path when `at:` is nil) → verify and handle
- Non-POST to matching path → 405 Method Not Allowed
- Any request to non-matching path → pass through

# `init`

Validates and normalizes plug options at compile/mount time.

Raises `NimbleOptions.ValidationError` if options are invalid.

---

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