All notable changes to this project are documented in this file.
The format is based on Keep a Changelog.
Unreleased
2.0.4 — October 9, 2026
Fixed
- Generated Oban workers no longer report Dialyzer pattern-match warnings for unsaved uniqueness conflicts. Oban's job type declares a persisted ID but its Basic engine can return an unsaved conflict; admission reads the ID through a private map helper. Retry limits, not-persisted refusals, identity checks, and runnable-state checks retain their behavior.
- The type gate now compiles the same generated worker used by the PostgreSQL suite. Real Oban.Basic transaction tests force an unsaved conflict, verify retry after the winning transaction commits, and verify refusal after all twenty retries.
- Dependency currency: Ash 3.34.4 → 3.34.6 (resolves EEF-CVE-2026-101028 in the locked graph) and AshPostgres 2.14.3 → 2.14.5.
2.0.3 — October 7, 2026
Fixed
- Delivery diagnostics survive recovery races, end to end. Re-admission no longer
wipes
last_error: a successful release after a durable enqueue replays the row's admission-time diagnostic (the story of the failure that scheduled the retry —http_503stays visible through the retry; only a fresh enqueue failure writes a new error). Every outcome that resolves the row's immediate fate —mark_succeeded,mark_send_failed(both the terminal and the retryable form),finalize_disable, and the stored 410 obligation itself (mark_disable_pending) — now RELEASES the enqueue claim in the same statement, so a reconciler whose enqueue raced the outcome finds the token gone. A release that then matches zero rows on a cleared claim classifies as BENIGN COMPLETION (:reconciled, never:endpoint_error); only a still-live foreign claim is a genuine:stale_enqueue_claim. And a successful release now PARKS the enqueue lease for one horizon (30s): a concurrent recovery sweep can no longer re-admit the row in the instant between the token clearing and the sweep ending — admission is exactly-once per recovery window at the seam itself (the concurrent-reconciler contract the suite pins, previously upheld only by SQLite's statement serialization; the final coverage gate caught the true-concurrency gap). The:release_enqueuemachine primitive gains an optionalenqueue_lease_expires_atargument (default nil — direct callers see no change). Consumer-observable consequence: a recovery sweep that runs inside a freshly admitted row's 30-second parked-lease horizon now reports that row as a contended:duplicatewhere 2.0.2 re-admitted it — sub-30s recovery cadences and:duplicate-count alerting will see the new shape. A row whose release reload finds it deleted or unreadable now reports:enqueue_reload_failedinstead of a stale-claim error. First reported by a sirtify consumer review, with the race family caught by the 2.0.3 release reviews reproducing it on real SQLite and real PostgreSQL. - Test: the 5xx backoff bound is asserted exactly (4..7) — the legacy
<= 8accepted an unreachable value.
2.0.2 — October 6, 2026
Fixed
- Outbound
Retry-Afteris honored on every retryable status, not only 408/429: a 5xx response carrying the header (the canonical 503 overload signal) now schedules the receiver's own recovery window; an absent or malformed header keeps the backoff fallback (first reported by a sirtify consumer review). Note the bound this moves: a header-bearing 5xx snooze is clamped byretry_after_cap_seconds(default 86,400s), notmax_backoff_seconds(default 3,600s) — an exhaust-fast adopter that lowered the latter but left the cap at its default will hold a 503-with-header attempt up to 24x longer per attempt. Lowerretry_after_cap_secondsif that posture must hold. - Test dependency currency:
ash_postgres2.14.2 → 2.14.3.
2.0.1 — October 6, 2026
The first published 2.x release includes the delivery ownership, recovery, and migration changes below. The guided tour keeps protocol implementations available for resources defined during notebook evaluation.
Fixed
- Disable protocol consolidation in the Livebook setup so dynamically defined Ash resources use their custom display without consolidation warnings.
- Update the notebook, release tooling, and production consumer to 2.0.1.
2.0.0 — October 6, 2026
Source candidate; superseded by 2.0.1 before Hex publication. The Git tag is retained.
Delivery rows now retain declaration and queue ownership, fence each send attempt, and recover unfinished work after queue exhaustion or an interrupted endpoint disable. HubSpot retries use a stable batch identity, and the default transport bounds the complete HTTP operation. Existing installations require the ordered schema and identity migration in UPGRADING.md before running 2.0 workers or ingress.
Changed
- Outbound rows persist declaration ownership and queue route. A declaration
cannot recover another declaration's rows. Named callbacks have stable routes;
anonymous callbacks can supply
enqueue_keyfor durable recovery. - Send and recovery admission use separate UUID tokens and expiring leases. Every result is fenced to the live attempt; retry ceilings are checked before sending. The complete ledger primary key is used throughout.
- A 410 response records
:disable_pendingbefore disabling the matching endpoint configuration. Recovery completes that obligation without another HTTP request. - Oban uniqueness covers runnable jobs and the durable route/tenant identity. Completed or discarded jobs do not strand recoverable rows; an unpersisted conflict is an admission error.
- HubSpot's default dedup identity canonicalizes the complete batch, ignoring only
each outer event's
attemptNumber. The optional providerevent_identity/1callback follows an explicit declaration extractor in precedence. Retained legacy rows are adopted explicitly, preserving payloads and an audit map. - Ash's supported floor is 3.34.3, including its security correction and the tenant inverse API used by workers. The lock resolves Ash 3.34.4, AshPostgres 2.14.2, and Spark 2.7.6.
Fixed
- Minimal production consumers start
:public_keyand:sslthrough the package's application dependencies; optional packages are unnecessary for default HTTPS. - Direct delivery calls apply the documented retry defaults for omitted or
nilsettings, including the attempt ceiling and boundedRetry-After, without requiring an Oban worker. - Required keyset pagination, bounded retention deletion, custom ledger keys, renamed UUID endpoint/subscription keys, and aliased worker HTTP-option resolvers.
- Endpoint status values are cast and constrained at compile time; empty enabled sets, overlapping states, and primary-key mappings are rejected.
- IDs on direct event structs and HTTP headers reject control bytes and malformed UTF-8. Telemetry reasons use a fixed vocabulary, preventing arbitrary tokens from escaping.
- SSRF classification covers special-purpose address ranges; IPv6 Host values are bracketed. Bounded HTTP follows interim responses to the final response, stops at its body cap, and uses one deadline across resolution and transport.
- HubSpot's finite event taxonomy includes generic object events.
Documentation and delivery
- Current install/version pins, terminal inbound acknowledgments, generated DSL references, migration instructions, and an executable 2.0.0 Livebook.
- Candidate archive inspection and notebook execution precede publication; the normal notebook then executes against the exact published version.
- Documentation changes trigger Linux CI. The minimum-consumer leg pins Ash
3.34.3; package, PostgreSQL, coverage, and Livebook checks feed
all-checks-pass.
1.3.0 — October 5, 2026
Additive options for domain naming and lifecycle, a compile
fix for generated (non-writable) primary keys, typed subscription
matching, loud documentation of the policy/id/adapter obligations, and a
Linux CI leg exercising an AshPostgres uuid_v7 consumer shape.
Added (additive options — non-breaking per ADR-0010)
payload_attributeon theoutbound_deliverysection. The exact payload-bytes column's name is configurable (default:payload): rename it when the consumer's domain reservespayloadfor its own sole payload store. The injected column, the:dispatchaccept list, and every signing/sending read follow the configured name; a name colliding with another injected field fails closed at compile.prune_action :noneon theoutbound_deliverysection. Opt out of the retentiondestroy :pruneaction's injection for append-only audit ledgers (no destroy action exists on the resource at all);AshHooks.Delivery.prune/2then fails loud with a named error — deletion is the consumer's own surface. Default:destroyis unchanged.status_attribute/enabled_values/disabled_valueon the newendpointsection. Map the durable enable/disable onto the consumer's OWN switch (e.g. anactiveboolean):statusis then not injected — one switch, so the consumer's off switch and the package's can never silently disagree. The dispatcher's skip and the send path's dead-letter both route throughAshHooks.Endpoint.enabled?/1; the 410 auto-disable flips the mapped attribute. The mapped attribute must be consumer-declared (fail-closed at compile).- A Linux CI leg with a Postgres service exercising the transformers
against an AshPostgres, uuid_v7-keyed, sole-store-payload consumer
shape (
postgresjob; the suite is env-gated byASH_HOOKS_POSTGRES=1and excluded from ordinary dev runs).ash_postgresis a dev/test-only dependency — never ships, never constrains consumers.
Fixed
:dispatch/:ingestno longer accept:idon resources whose primary key is a non-writable:id(e.g.uuid_v7_primary_key): Ash'sValidateAcceptraised at compile on exactly that consumer shape, making the extensions unadoptable without replacing the PK. The accept lists carry:idonly when the PK is a writable:id(the package-injected default — byte-identical classification); the transformer's decision is persisted and the runtimes read it back, so the compiled action and the classification path can never disagree (composite and non-:idprimary keys included). For the non-writable shape, created/duplicate classification pre-reads the unique identity instead: exact in every sequential case (provider redeliveries, producer re-fires) and wherever the primary read can see the existing row (a base-filtered read that hides it classifies:created). Under a misclassification the storage upsert preserves one row.A double send stays impossible through theCorrection in 2.0.0: the 1.3 outbound state transition did not fence every late result or expired owner. Version 2.0 adds attempt tokens, leases, and separate enqueue claims; network sends remain at-least-once and require receiver deduplication. In 1.3 the enqueue seam was the configured enqueuer's own fence — the default Oban enqueuer's endpoint_id+event_uuid job uniqueness, or the consumer's own dedup for a custommark_sendingCAS and inbound claim fence.enqueue:. ADR-0010 note: the injected action accept lists are covered surface — this is a behavior correction (the previous accept list required a writable PK that consumers with generated PKs cannot provide), not a removal; no working consumer can regress (the shape did not compile).- Covered-surface tightening alongside the H4 mapping: the send path
dead-letters an endpoint whose status is anything other than
:enabled(previously only an exact:disableddead-lettered, so a consumer-redeclaredstatuswith extra values delivered). The dispatcher's skip and the send path now route through the ONEAshHooks.Endpoint.enabled?/1check — fail-closed by design. AshHooks.Subscription.matches?/2normalizesevent_typesentries withto_string/1. An atom-typed register ({:array, :atom}— a closed enum) previously matched NOTHING: zero deliveries, no error. Atom and string entries (and the"*"/:"*"wildcard in either representation) now match identically. Consumers redeclaringevent_typeskeep their own constraints — the injected["*"]default is not forced on them.
Changed
- CI is Linux-only; the macOS and Windows build legs are removed. Tri-OS stays a developer-portability requirement (clone, set up, and test must work on every OS) — it was never meant to be a CI topology, and the README no longer claims dedicated per-OS legs. Also hygiene: the workflow's pinned GitHub Actions (checkout, cache, upload-artifact) moved off the Node 20 runtime to their current releases. No library code changed.
- The DSL cheat-sheet drift gate is no longer vacuous. The package
leg's
mix spark.cheat_sheets --checkran without an extension list and checked nothing; it now passes the five extensions explicitly, so a staledocumentation/dsls/actually fails the build (the sheets in this release are regenerated under that invocation). - Dev/test
mintbumped 1.10.1 → 1.11.0 (EEF-CVE-2026-94194 / -91043 / -92103, via the dev-only igniter→req→finch tree; never ships in the package).ash_postgres2.13.1 is added dev/test-only for the Postgres leg above.
Docs
- Policy obligation at the extension site (
OutboundDelivery,Endpointmoduledocs): the injected write actions are named and the consumer-owned policy obligation is stated loudly — the inbound half's "you write your own" posture, mirrored outbound. The seven delivery actions plus the endpoint's:disable. - Deterministic
Event.idguidance (Event,Dispatchermoduledocs): derive the id from the artifact's stable id — a generated id turns every producer re-fire into a duplicate POST per sweep. - The adapter-author contract (
AshHooks.Httpmoduledoc): headers must be a LIST of{name, value}tuples (a map silently losesRetry-After), and the resolve-once/connect-pinned obligation that closes the DNS-rebinding TOCTOU is the adapter's, with the driver's send-time check as the residual. retry_after_cap_secondssemantics (Workermoduledoc): the 86,400 default is receiver-held-state budget — up to 24 hours per attempt on one header; lower it for an exhaust-fast posture.- The 410 auto-disable posture (
Deliverymoduledoc): an unattributed system bulk write by design (no actor in a versioned register), no built-in tenant-facing signal — a receiver that answers 410 once goes dark silently unless the app wires the[:ash_hooks, :delivery, :disable]telemetry event or accepts the posture in writing.
1.2.1 — September 24, 2026
Fixed
- The documentation tree now ships in the hex tarball. 1.2.0's package
carried only the top-level docs (README, CHANGELOG, UPGRADING, usage-rules),
so
documentation/— including the multi-tenant adoption checklist — was reachable only on GitHub/hexdocs while UPGRADING.md and the CHANGELOG referenced it by repo-relative path. Installed-package readers now get the tutorials (get-started, tenancy-adoption-checklist), the DSL reference, and the Livebook alongside those references. No library code changed; hexdocs and the package page README are unaffected.
1.2.0 — September 24, 2026
Tenancy release (ADR-0011, additive and opt-in): ash_hooks aligns with Ash attribute multitenancy — an explicit tenant threads every data call, and the package fails closed with named errors before any data access when a multitenant resource is touched without one. Single-tenant adopters change nothing: the entire pre-1.2 suite passes unchanged over tenant-less fixtures inside the now-610-test gate, because threading a tenant through resources with no multitenancy declaration is mechanically inert.
Added
- Tenant threading on every path —
AshHooks.dispatch/4(:tenantopt),AshHooks.Ingress.ingest/4(ctx[:tenant]), the claim/mark/ renew/redact/prune/reap heads (:tenantopt, new optional arity),AshHooks.Delivery.run/2(tenant recovered from job args), and both prunes. The tenancy contract: consumers declaremultitenancy :attribute, attribute: <same attr>on the resources a given operation touches; the package verifies that set on every entry point and returns{:error, :tenant_required}/{:error, :tenancy_mismatch}before any data access (Ash's native fail-closed stays the backstop;global?: trueis rejected — it silently disables fail-closed reads). A compile-time verifier additionally rejectsmultitenancy :bypass,:bypass_all, and:allow_globalactions on multitenant package resources. - Per-tenant operations —
AshHooks.reap_all/2andAshHooks.prune_all/2sweep a tenant enumerable sequentially with per-tenant error isolation and totals ({:ok, %{results: %{tenant => {:ok, n} | {:error, reason}}, total: n}}). AshHooks.reconcile_pending/3— the supported reconciliation for delivery rows stranded at:pendingby a crash between row write and enqueue: a WHERE-gated CAS flip to:enqueue_failed(real state change — matched-records is the win signal; exactly one winner per row among concurrent reconcilers, proven under a custom non-Oban seam), then enqueue through the same seam dispatch takes. Exactly-once ACROSS claim mechanisms (reconciliation vs re-dispatch repair) is the enqueue seam's contract — the canonical Oban seam's job uniqueness provides it; a custom seam must be idempotent itself.- Tenant-aware secret resolution (all optional) — the worker macro's
:tenant_aware_secrets(2-arityf(ref, tenant)resolver contract); the inboundsecret fn tenant -> ... endsource; the provider behavior's optionalwebhook_signing_secret/2(Organization × connection custody;use AshHooks.Providerships an overridable default delegating to/1); and:tenantin theverify_signature/3context map. - Oban args carry the tenant — the generated
enqueue/2serializes the row's tenant (string tenants are the supported shape; non-string tenants must round-tripparse_attributeover JSON); uniqueness keys stay[:endpoint_id, :event_uuid], and a pre-tenancy tenant-less job still conflicts with the tenant-bearing job on the same pair (proven on the real Oban engine). - The dispatch result contract is now a public typespec
(
AshHooks.Dispatcher.dispatch_result/0) including the new:reconciledstatus. - The multi-tenant adoption checklist (documentation/tutorials/tenancy-adoption-checklist.md) and ADR-0011.
Migration notes
Two adopter classes see action; single-tenant staying single-tenant sees none. See UPGRADING.md and the adoption checklist: (1) cutover with in-flight 1.1.x Oban jobs — drain before serving multitenant dispatch (pre-tenancy args fail closed, correct but noisy); (2) single→multi on populated tables — backfill the tenant attribute first (NULL-tenant rows form a shadow partition that strands effect-once), then regenerate identity indexes, then enable.
1.1.1 — September 16, 2026
Tri-OS proof release. No covered-surface change (patch, ADR-0010): the package now carries mechanical proof of macOS, Linux, and Windows support — a dedicated CI leg per OS runs the full 555-test suite on the dev-default toolchain, all nine legs green, and the README claims exactly the OSes the legs prove.
Added
- CI legs on macOS and Windows (dev-default toolchain Elixir 1.20.4 /
OTP 28,
compile --warnings-as-errors+ the full suite). Linux was already proven by every existing leg. .gitattributes(* text=auto eol=lf): every OS checks out byte-identical sources; all tracked files were already LF, so no renormalization was needed.
Fixed
- Four test-side portability defects exposed by the first Windows run
(the package and its sqlite NIF substrate compiled and ran on
Windows throughout — every defect was in fixtures):
- two
:inetshttpd fixtures hardcodedserver_root: "/tmp", nonexistent on Windows — one failure hid as asetup_allinvalidation that silently excludes a whole module's tests from the failed count; both now useSystem.tmp_dir!(); - CRLF checkout on Windows broke the install-task fixture's byte
assertions (
=~ "plug Plug.Parsers\n"vs"\r\n"bytes); - httpd's URL→path translation mangles backslashes, so every request
returned 500 on Windows — the fixtures now hand httpd
forward-slash
server_root/document_root(a no-op on unix).
- two
1.1.0 — September 16, 2026
Support-window release: the Elixir floor rises to 1.20 (a minor, not a
major, by ADR-0010 rule 4 — the first exercise of that rule), the
repo's own toolchain becomes self-enforcing and lockstep-pinned, and
dependencies sit at latest with mix hex.audit running in CI.
Changed
- The supported Elixir window is now
~> 1.20(was~> 1.17) — nothing below Elixir 1.20 is supported, by owner decision; the OTP floor is 28 (CI tests 28 and 29). Consumers on 1.17–1.19 get a resolver-level refusal instead of a compile. ADR-0010 rule 4 makes a supported-floor bump a MINOR release, so a~> 1.0pin resolves 1.1.0 and then fails resolution on old Elixirs — pin{:ash_hooks, "~> 1.0.4"}until you can move (see UPGRADING.md). CI tests the window on Erlang/OTP 28 (the dev default, pinned in.tool-versionsand mirrored by a dedicated leg) and 29; the floor leg drops the lock and resolves at the floor to keep the requirement honest. - Repo-local toolchain enforcement — consumers are unaffected:
config/is excluded from the hex tarball. Development runs on one pinned toolchain (.tool-versions: Elixir 1.20.4 / Erlang/OTP 28) mirrored by a dedicated CI leg;config/config.exsrefuses any OTP release CI does not test (currently 28/29) before anything compiles, becauseSystem.version/0does not encode the OTP build and a same-Elixir foreign build would otherwise compile incompatible BEAMs silently. The window,.tool-versions, the OTP allowlist, and the CI matrix move together in ONE commit.mix hex.auditnow runs in CI on every push. - Dependencies moved to latest (
mix hex.outdatedshows zero "Update possible"): oban 2.23.1 → 2.24.1, dialyxir 1.4.7 → 1.4.8, ex_doc 0.40.3 → 0.40.4.mix hex.auditclean before and after (and OSV shows no advisories against the three new versions).
Fixed
- A
%Regex{}inside a module attribute (@redaction_patterns) embeds the compiledre_pattern, which is a reference on OTP 28 — Elixir < 1.19 cannot escape references when injecting an attribute into a function body, so the package did not compile for consumers on 1.17–1.19 × OTP 28 (a combination the old window admitted). The patterns are now built by adefp(identical regexes, flags, and order); with the floor now at 1.20 that combination is out-of-window, and thedefpkeeps the escape class dead if the floor ever drops below 1.19. mix dialyzeron a fresh PLT no longer reports 13 unknown-function/ unknown-type warnings intest/support:use AshSqlite.RepoemitsEcto.Adapters.SQLdelegations, and the deps-PLT app enumeration does not reliably include:ecto_sql— it is now explicit inplt_add_appswith the reason inline.
Added
scripts/check-currency.sh: exits nonzero whenevermix hex.outdatedshows resolvable drift, and prints the packages whose latest release the resolver will not take under current requirements — each of those must carry an inline deliberate-pin reason inmix.exs. Fail-closed: an erroredhex.outdatedrun (lock mismatch, resolver failure) also exits nonzero.
1.0.4 — September 16, 2026
Current-Ash compatibility release: the repo's own app surfaces carry Ash
3.33's required default_string_length_count, and the byte-bound
correction family that requirement exposed is fixed.
Fixed
- Delta-review completions of the byte-bound invariant, each red-proven:
Event.new/1and the dispatcher's event guards bound ids/types by BYTES, notString.length(graphemes) — under Ash 3.33:codepoints, a 128-grapheme combining-character type (512 bytes) passed validation and then failed every endpoint's:dispatchcreate. The inbound side already bounded bytes; outbound now matches, in either counting mode.- A thrown 255-character token no longer overflows
last_error:"throw: " <> classify_token(...)could reach 262 bytes and fail the enqueue-failure ledger write itself (mode-independent, ASCII); the combined string is capped now, like the:exitsibling. - An inbound handler failing with an invalid-UTF-8 binary no longer
loses its failure record: the error class collapses to the
redaction floor's
[binary]placeholder instead of failing the:mark_failedwrite and leaving the delivery re-drivable.
- Captured snippets and error summaries are now capped in BYTES instead of
grapheme-sliced. Under Ash 3.33's
:codepointscounting the injectedmax_lengthconstraints count codepoints, and a grapheme-sliced 2048-character snippet of combining characters spans 4000+ codepoints — the post-send ledger write then violated its own constraint and crashed (mark_succeededfailed on a hostile response body), the same re-send poison class the control-byte strip closes. The byte cap (on a codepoint boundary) bounds the value under every counting mode, on every supported Ash. Covers the response-snippet capture path, the redaction floor, and the inbounderror_class/ dispatcher exit-string bounds. Found by mining a timed-out cross-vendor review probe; regression is red-proven against the grapheme-sliced code. - Current-Ash compatibility: Ash 3.33 requires every application that
compiles resources to set
config :ash, :default_string_length_count(the app-level half of GHSA-cwjv-574p-59f6), which broke the CI floor and Livebook legs — both resolve Ash lock-free at latest and failed resource compilation. The test application (config/test.exs, repo-local and never shipped), the get-started Livebook, and the tutorial now carry the setting. The package itself still writes no:ashconfiguration — consumers keep that choice, and both documented values work (see UPGRADING.md). A new tripwire test locks the boundary:lib/never writes:ashapplication environment. - The tutorial's requirements line now matches the tested floor (Elixir
~> 1.17; the~> 1.15claim was disproven by the CI floor leg, per ADR-0010).
Changed
mix.lock: ash 3.33.4, and the dev/test substrate cleared of its published advisories (ash_sqlite 0.2.19 / ash_sql 0.7.5 — test-only, never shipped in the package;mix hex.auditnow clean).
1.0.3 — August 23, 2026
Docs-and-tests release (no functional changes; 1.0.2's gate and fixes unchanged):
Changed
- The send-failure contract tests dropped their 512MB bodies for 16MB with the peer's receive window pinned to 1KB — the same in-flight guarantee at 1/32nd the allocation, closing the resource-pressure residual from the 1.0.2 ship report. CI uploads the cover HTML when the coverage gate reds (triage data, not guesswork).
Docs
- UPGRADING carries the 1.0.2 behavior notes (the
:httpcliteral-IP HTTPS refusal, the:http_optsthreading). - The
:httpcandBoundedmoduledocs document the literal-IP posture and the:cacertstrust-store seam; ADR-0009 records the seam decision (wayfinder D3) in Consequences.
1.0.2 — August 22, 2026
The 100% coverage release: the maintainership directive (2026-08-22)
superseding 1.0.1's print-only posture, worked through the wayfinder
decisions (#20-#22) — line coverage of lib/ now GATES in CI, and the
release carries the surfaces that build required.
Added
Target.ssl_options/2and both HTTP adapters accept an injectable:cacertstrust bundle ([cacerts: der_list]in the adapter opts — on the Oban path,use AshHooks.Worker, http_opts: ...): pin a private CA for private-CA endpoints. Default UNCHANGED (the OTP CA store). The worker'shttp_optstakes compile-time literals, or an{m, f, a}resolved per-perform for computed bundles.- TLS success paths are covered by REAL CA-verified loopback sessions against committed local fixtures, including the literal-IP iPAddress-SAN floor and its fail-closed mismatch.
Fixed
AshHooks.Http.CertSan.ip_san_match?/2fails closed (returnsfalse) instead of CRASHING its caller when a certificate's SAN extension carries non-DER bytes: asn1's decode EXITS on invalid tags, which the previous error-only rescue could not catch.AshHooks.Http.Httpc(the alternate adapter) refuses literal-IP HTTPS destinations with{:error, :ip_literal_https_needs_bounded}: it never holds the socket, so the iPAddress-SAN floor cannot run there and chain-validation alone would let any chain-valid certificate authenticate the endpoint IP. UseAshHooks.Http.Bounded(the default) for literal-IP HTTPS endpoints. Cross-vendor-reviewed (both peers), pre-existing hazard class.use AshHooks.Workerthreads:http_optsinto the delivery config (the option was previously accepted and silently dropped).
Changed
mix test --coverGATES at 100% line coverage oflib/'s runtime-reachable lines (CI fails on a miss). The ignore list carries exactly the:cover-invisible classes: Spark's generated DSL sections, test-support modules, and four compile-window module-body lines. 31 provably-dead defensive arms were DELETED with proofs (commit d6f3788) rather than exempted; the shipped surface behaves identically.
1.0.1 — August 22, 2026
Docs-and-tests release (no functional changes). The 1.0.0 ship report's three residual postures are probed and pinned instead of documented:
Added
- The fenced ledger's exactly-once claim is now pinned by a TRUE
concurrent race test (8 simultaneous same-key ingests on a
3-connection sqlite pool: exactly one
:created, one row) — and the same race without the storage unique index proves the failure mode: sqlite fails CLOSED with a loud storage error, zero rows. TheInboundDeliverymoduledoc cites both. - The
:httpcadapter's giant-non-2xx window is now MEASURED (dribble probe: ~4.65MB transient for a 4MB error body against a 16-byte bound) with a committed containment test pinning the final cut; ADR-0009 carries the numbers.
Fixed
- The missing-index hazard is documented accurately: on storage layers
with native conflict support (sqlite, postgres) a missing unique
index errors the ingest loudly — the previously documented
"two rows, both
:created" shape applies to degraded-upsert layers. The as-built floor is stronger than the docs claimed.
1.0.0 — August 22, 2026
1.0.0 is the semantic-versioning baseline (ADR-0010): no public API removals or renames from 0.2.x. Three behavior corrections below; migration notes in UPGRADING.md.
Fixed
- Truncated chunked responses no longer classify as success. The
default HTTP adapter returned a partial body as
{:ok, ...}when a chunked response was cut by early close — a 2xx on partial bytes could mark a delivery:succeeded. Chunked now returns{:error, :truncated_body}exactly like the Content-Length framing, and the driver retries. - The literal-IP https certificate check actually works now. The
iPAddress-SAN matcher was unreachable since birth (found by the new
dialyzer gate): every literal-IP https endpoint was rejected
fail-closed with
:cert_ip_mismatch. Extracted to the fixture-testedAshHooks.Http.CertSan(ADR-0009). The IPv6 direction initially matched nothing (a cross-vendor review finding, fixed before release) — both address families now verify against fixture certificates. - A hostile chunked response can no longer balloon worker memory.
The chunk consumer accumulated the ENTIRE attacker-declared chunk
before trimming to the body bound — an 8MB declaration held ~16.8MB
in the worker against a 16-byte bound (cross-vendor security review,
executable probe). Consumption is now phase-bounded: keep at most the
allowance, discard the excess slice-wise (one receive slice held at a
time), and a chunk terminator that is not CRLF is now
{:error, :malformed_chunked}instead of being silently consumed (a malformed 2xx can no longer classify as succeeded). Run-on chunk-size lines without a terminator are refused past a sane bound instead of being buffered. AshHooks.Delivery.prune/2returns{:error, error}when the resource lacksinserted_at— the same contract asAshHooks.Ingress.prune/2its@specalways promised (previously raisedArgumentError).
Added
- Semver and support policy (ADR-0010): the covered public surface is named, deprecations run two minors, and the minimum supported versions are CI-tested (Elixir ~> 1.17 / OTP 27+ / Ash ~> 3.0 / Oban ~> 2.20 optional). The previously claimed Elixir 1.15 floor was never buildable with current Ash and is corrected here — 1.0.0 is the first release whose floor is actually tested.
- SECURITY.md with a private disclosure channel (GitHub private vulnerability reporting, enabled), scope, and known posture notes; CONTRIBUTING.md and UPGRADING.md — all three ship in the tarball and render on hexdocs.
- Dialyzer gate on the public API (local + CI) — first run caught the dead IP-SAN matcher above.
- CI: a floor leg resolving dependencies at the declared minimums (it disproved the old 1.15 claim on its first run), a package leg checking the hex tarball ships every documentation file and that the DSL cheat sheets match the DSL, and an advisory coverage report.
@specon the HTTP behavior'srequest/5(both adapters),AshHooks.dispatch/4, the resource extensions'statuses/0/signing_modes/0(now documented), and the internal HTTP target-resolution helpers.- A dedicated extension-shape test suite for
AshHooks.OutboundDelivery, pinning the injected attributes, theunique_deliveryidentity, and the:dispatchno-touch-upsert contract.
Changed
- Read posture documented honestly (ADR-0005 amendment): read access to the ledger/delivery resources is consumer-governed — the package injects no read policies. README → Security carries the deny-by-default policy recipe; the resource moduledocs now carry READ-EXPOSURE warnings. (No code change: reads were always consumer-governed; the docs previously implied otherwise.)
- Install guidance is
{:ash_hooks, "~> 1.0"}; the README (previously pinned~> 0.1.0, one feature-release behind) and tutorial both corrected, with the install-constraint check added to the release checklist.
0.2.2 — August 22, 2026
Fixed
- usage-rules.md now ships in the hex tarball (the Ash AI-assistant convention reads it from the package) and renders on hexdocs. It was previously GitHub-only.
0.2.1 — August 22, 2026
Added
- A runnable get-started Livebook (guided tour: inbound verify/dedup, a live local delivery, telemetry, retention) — CI-verified headless on every push. No functional changes.
0.2.0 — August 22, 2026
Added
- Retention hooks (ADR-0005 floor completed):
AshHooks.Ingress.prune/2andAshHooks.Delivery.prune/2(terminal rows older than a cutoff, keyed on the resource'stimestamps(); non-terminal rows never deleted), andAshHooks.Ingress.redact_payload/4(payload field-redaction under the claim fence; the original-bytes digest is preserved). Deleting a terminal row re-opens its dedup identity — set TTLs beyond replay/re-emission horizons.
0.1.1 — August 22, 2026
Changed
- README rewritten as a package front door (no functional changes): audience/register pass; internal build references removed.
0.1.0 — August 22, 2026
Added
- Inbound machine:
AshHooks+AshHooks.InboundDeliveryextensions,AshHooks.Ingress(verify-before-trust, fenced unique-ingest dedup ledger, lease-based claim, crash-window re-drive), provider behavior with ComplyCube/HubSpot v3 references and a test provider. - Outbound machine:
AshHooks.dispatch/4fanout with per-endpoint isolation and enqueue-repair CAS;AshHooks.OutboundDelivery,AshHooks.Subscription,AshHooks.Endpointextensions. - Delivery runtime (
AshHooks.Delivery) + host-injecteduse AshHooks.Worker: Standard-Webhooks signing (standard/dual/legacy envelopes), row-owned retry policy (Retry-After, jittered backoff, dead-letter ceiling), 410 durable disable, redirect refusal, send-time SSRF re-check. AshHooks.Httpadapter behavior with a memory-bounded native HTTP/1.1 default (AshHooks.Http.Bounded— every read capped under all framings) and an OTP:httpcalternative.- Response-snippet redaction floor (ADR-0005 amendment): no body bytes
by default (fixed-grammar status + allowlisted content-type summary);
per-call
snippet_captureopt-in under the in-package redaction floor (NFKC homoglyph folding, bounded-fixpoint decode chain, separator-tolerant markers, ≥16-char union-alphabet entropy rule); fail-closed consumersnippet_redactorcallback;[captured]marking. - Telemetry (ADR-0005 floor: ids/integers/fixed atoms/classified
reasons only — never secrets or bodies): ingress verify/dedup/claim,
dispatch enqueue_failed, delivery attempt/result/backoff/dead_letter/
disable;
AshHooks.Telemetry.fingerprint/1. - Igniter installer, DSL cheat sheets, get-started tutorial, usage-rules.md.
Security
- Package floors shipped in-code (ADR-0005): secrets as sources only (literals rejected at parse), default-deny machine-written ledger fields, SSRF guard at registration and send, NO headers stored on either ledger at all, no response-body persistence by default, classified-only error strings.