# `AshHooks.Provider`
[🔗](https://github.com/baselabs/ash_hooks/blob/v2.0.3/lib/ash_hooks/provider.ex#L1)

Behavior for inbound webhook providers.

A provider verifies a request's signature, parses its event type, and handles
its decoded payload. Optional callbacks resolve per-connection secrets,
declare an authenticated timestamp, or identify a complete delivery.

`verify_signature/3` implementations whose scheme is exactly "lowercase-hex
HMAC over the raw body" delegate to `default_verify_signature/4`;
scheme-specific providers (composite strings, separate timestamp headers,
replay windows) implement their own.

Built-in providers return typed events. Implement `handle_event/2` in your
own provider when ingress must execute application actions. Make those
actions idempotent: durable ingress claims prevent overlapping ownership,
but cannot make a remote side effect and the ledger update one transaction.

# `parse_error`

```elixir
@type parse_error() :: :unknown_event_type | :malformed_payload
```

# `raw_body`

```elixir
@type raw_body() :: binary()
```

# `signature_algorithm`

```elixir
@type signature_algorithm() :: :hmac_sha256 | :hmac_sha512
```

# `signature_error`

```elixir
@type signature_error() :: :invalid_signature | :no_webhook_secret | :stale_timestamp
```

# `signature_header_value`

```elixir
@type signature_header_value() :: binary()
```

# `verify_context`

```elixir
@type verify_context() :: %{
  optional(:replay_window_seconds) =&gt; pos_integer(),
  signature: signature_header_value(),
  headers: %{optional(String.t()) =&gt; String.t()},
  method: String.t() | nil,
  request_uri: String.t() | nil,
  tenant: term()
}
```

The request context `verify_signature/3` receives as its second argument.
Carries everything any provider's signature scheme can need:

  * `:signature` — the value extracted from the provider's signature header.
  * `:headers` — the full lowercased request header map, for schemes that
    read an additional header (e.g. a separate timestamp header).
  * `:method` — the uppercase HTTP method, for schemes that sign over it.
    `nil` when the caller does not supply it.
  * `:request_uri` — the exact request URI the provider signed. `nil` when
    not supplied.
  * `:tenant` — the ingest tenant when the ledger is multitenant, `nil`
    otherwise. A provider whose custody is per-tenant (per-connection
    secrets keyed by organization) reads it here.
  * `:replay_window_seconds` — the declaration override when configured;
    absent when the provider should use its own default.

A provider whose scheme needs only the signature reads `:signature` and
ignores the rest.

# `webhook_secret`

```elixir
@type webhook_secret() :: binary()
```

# `webhook_secret_scope`

```elixir
@type webhook_secret_scope() :: :app_level | :per_connection
```

Where a provider's webhook signing secret lives:

  * `:app_level` — one app-wide shared secret signs every connection's
    webhooks. The default when the callback is not implemented.
  * `:per_connection` — each connection carries its own signing secret.

# `event_identity`
*optional* 

```elixir
@callback event_identity(payload :: map() | list()) ::
  {:ok, String.t()} | {:error, :malformed_payload}
```

Returns a stable provider-defined identity for a decoded webhook payload.

This callback is optional. Ingress uses it only when the inbound declaration
has no explicit host `event_id` extractor. An implemented callback's error is
authoritative and never falls back to the raw-body digest. Providers without
the callback retain the raw-body SHA-256 identity behavior.

# `handle_event`

```elixir
@callback handle_event(event_type :: atom(), payload :: map() | list()) ::
  {:ok, typed_event :: struct()} | {:error, :retry | :permanent, term()}
```

Converts a verified, parsed payload into the provider's typed event.

# `parse_event_type`

```elixir
@callback parse_event_type(payload :: map() | list()) ::
  {:ok, atom()} | {:error, parse_error()}
```

Parses the event type from the decoded request body — a map for
object-shaped vendors, a list for batch vendors (HubSpot delivers
top-level arrays). Providers receiving a shape their vendor never sends
fail closed through their catch-all clause (`{:error, :malformed_payload}`).

# `timestamp_header`
*optional* 

```elixir
@callback timestamp_header() :: String.t() | nil
```

The request header this provider's signature scheme carries a trustworthy
timestamp in (`nil` when the scheme has none). A DSL `replay_window_seconds`
requires a non-`nil` value here — the compile-time verifier rejects the
combination otherwise, and there is no replay protection without it.
Optional, defaulting to `nil`.

# `verify_signature`

```elixir
@callback verify_signature(raw_body(), verify_context(), webhook_secret()) ::
  :ok | {:error, signature_error()}
```

# `webhook_secret_scope`
*optional* 

```elixir
@callback webhook_secret_scope() :: webhook_secret_scope()
```

Declares whether this provider's webhook signing secret is app-level
(the default) or per-connection. Optional.

# `webhook_signing_secret`
*optional* 

```elixir
@callback webhook_signing_secret(connection :: struct()) ::
  {:ok, webhook_secret()} | {:error, :no_webhook_secret}
```

Resolves the signing secret used to verify an inbound webhook.

The secret SOURCE is provider-dependent, so it lives with the provider
module rather than in the generic ingress. App-level providers read an
app-wide config value; per-connection providers read it off the `connection`
argument. Returns `{:error, :no_webhook_secret}` when unconfigured — the
caller verifies nothing and does not run the handler.

# `webhook_signing_secret`
*optional* 

```elixir
@callback webhook_signing_secret(connection :: struct(), tenant :: term()) ::
  {:ok, webhook_secret()} | {:error, :no_webhook_secret}
```

Tenant-aware per-connection secret resolution (optional, 1.2.0): resolves
the signing secret from the connection AND the ingest tenant — custody of
Organization × connection. A provider using `use AshHooks.Provider` gets a
default implementation delegating to `webhook_signing_secret/1` (existing
providers compile unchanged); a per-tenant provider overrides `/2`.

# `__using__`
*macro* 

The `use` form for new providers: sets the behavior and provides the
tenant-aware `webhook_signing_secret/2` as an overridable default that
delegates to `webhook_signing_secret/1` when the provider implements it
— implement `/1` and both shapes resolve; override `/2` for
Organization × connection custody; implement neither and the default
fails closed as `{:error, :no_webhook_secret}` (the optional-callback
semantics — an app-level provider never sees the callback called at
all). Providers written against the plain `@behaviour` keep working
unchanged (`webhook_signing_secret/3` resolves both).

# `default_verify_signature`

```elixir
@spec default_verify_signature(
  raw_body(),
  signature_header_value(),
  webhook_secret(),
  signature_algorithm()
) :: :ok | {:error, :invalid_signature | :no_webhook_secret}
```

Default HMAC verify implementation: lowercase-hex HMAC of the raw body under
the secret, compared in constant time behind a byte-size guard.

The byte-size guard both short-circuits wrong-length signatures and
satisfies `:crypto.hash_equals/2`'s equal-length requirement.

An EMPTY secret fails closed as `{:error, :no_webhook_secret}` — an HMAC
under the empty key is computable by anyone, so a secret source that quietly
resolves to `""` must not verify anything.

# `event_identity`

```elixir
@spec event_identity(module(), map() | list()) ::
  :not_supported | {:ok, String.t()} | {:error, :malformed_payload}
```

Resolves the optional stable event identity callback.

`:not_supported` means the provider leaves identity to the raw request
digest. Callback errors are returned unchanged for ingress to reject.

# `secret_scope`

```elixir
@spec secret_scope(module()) :: webhook_secret_scope()
```

Resolves a provider's webhook secret scope, defaulting to `:app_level` when
the optional `webhook_secret_scope/0` callback is not implemented.

Loads the provider module first: `function_exported?/3` reports false for a
module that is not yet loaded, which would misread an unloaded
per-connection provider as app-level.

# `timestamp_header`

```elixir
@spec timestamp_header(module()) :: String.t() | nil
```

Resolves a provider's trustworthy timestamp header, defaulting to `nil`
when the optional `timestamp_header/0` callback is not implemented.

Loads the provider module first — same unloaded-module guard as
`secret_scope/1`.

# `webhook_signing_secret`

```elixir
@spec webhook_signing_secret(module(), struct(), term()) ::
  {:ok, webhook_secret()} | {:error, :no_webhook_secret}
```

Resolves the per-connection signing secret for an ingest: an overriding
`webhook_signing_secret/2` (tenant-aware custody) wins; otherwise the
call delegates to `webhook_signing_secret/1` — providers of either
implementation shape work through this one entry point.

---

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