SkillKit.Webhook.Idempotency (SkillKit v0.1.0)

Copy Markdown View Source

Bounded ETS-backed dedup cache for webhook deliveries.

Vendors retry deliveries on non-2xx responses (Stripe for 72h, Slack continuously). When a registration opts into idempotency, this module remembers the delivery key for a TTL window so repeat deliveries return 200 without re-dispatching to the agent.

Key sources supported in the registration config:

  • %{"header" => "x-github-delivery"} — read from request headers.
  • %{"json_path" => "$.id"} — read from the raw body interpreted as JSON (top-level field only; full JSONPath is out of scope for v1).

Entries are swept lazily on access — each check/3 call checks the stored expires_at and treats an expired entry as first-sight.

Summary

Functions

Checks whether key has been seen within the TTL window.

Returns a specification to start this module under a supervisor.

Extracts the idempotency key from the request per the registration's key config. Returns :no_key when idempotency is not configured, or {:error, :missing} when configured but the key is absent.

Types

config()

@type config() :: %{
  optional(:key) => %{optional(String.t()) => String.t()},
  optional(:ttl) => pos_integer()
}

Functions

check(name, key, opts)

@spec check(atom(), String.t(), keyword()) :: :ok | :duplicate

Checks whether key has been seen within the TTL window.

Returns :ok on first sight (and records the key) or :duplicate if the key is still valid.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

extract_key(conn, arg2)

@spec extract_key(Plug.Conn.t(), config() | nil) ::
  {:ok, String.t()} | :no_key | {:error, :missing}

Extracts the idempotency key from the request per the registration's key config. Returns :no_key when idempotency is not configured, or {:error, :missing} when configured but the key is absent.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()