AshHooks.InboundDelivery (AshHooks v1.0.1)

Copy Markdown View Source

Turns the consumer's resource into the inbound webhook ledger — the durable, fenced dedup substrate of the inbound pipeline (ADR-0003).

Attach next to the consumer's own data layer:

use Ash.Resource,
  data_layer: AshSqlite.DataLayer,
  extensions: [AshHooks, AshHooks.InboundDelivery]

inbound_delivery do
  scope_identity([:account_id])
end

The extension injects the ledger fields (provider, external ids, payload + digest, the fenced state machine's status/token/lease/attempts), the unique_ingest identity spanning provider + external event id + the declared scope slots, and the fenced action primitives (:ingest, :claim, :mark_processed, :mark_failed, :renew).

The uniqueness identity must be backed by a REAL unique index on the consumer's data layer — storage-level uniqueness is the idempotency primitive, and the fenced machine's crash-safety rests on it (PINNED by test: 8 simultaneous ingests of one event id converge to exactly one row and one :created; the same race on a table without the index fails closed with a loud storage error — test/ash_hooks/ingress_race_test.exs). Scope slots must be non-nullable attributes: a nullable slot would make nil scope values distinct on SQL unique indexes and silently break dedup for scope-less redeliveries.

The injected actions are PRIMITIVES, not fences: the conditional gates (claim only from :received or an expired lease; mark/renew only by the current token under an unexpired lease) live in the query filters that AshHooks.Ingress builds — the WHERE clause is the portable fence (probe 2026-08-21: error()-in-expression atomics are inexpressible on sqlite, and action-level change filter(...) is silently dropped on the atomic path).

READ EXPOSURE: this ledger stores RAW provider payloads (third-party PII), event ids, and scope keys. The package injects NO read policies — read access is governed ENTIRELY by the consumer's own domain policies. Mount the ledger behind policies that deny reads by default and open them explicitly (README → Security has the recipe).

Summary

Functions

The fenced state machine's statuses, in lifecycle order.

Functions

inbound_delivery(body)

(macro)

statuses()

@spec statuses() :: [atom()]

The fenced state machine's statuses, in lifecycle order.