Skip to content

Releases: baselabs/ash_hooks

ash_hooks 2.0.4

Choose a tag to compare

@palermo-git palermo-git released this 09 Oct 17:18

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.

AshHooks 2.0.1

Choose a tag to compare

@palermo-git palermo-git released this 07 Oct 01:04

2.0.1 — October 6, 2026

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_key for 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_pending before 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 provider event_identity/1
    callback 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_key and :ssl through the package's
    application dependencies; optional packages are unnecessary for default HTTPS.
  • Direct delivery calls apply the documented retry defaults for omitted or nil
    settings, including the attempt ceiling and bounded Retry-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.1 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.
  • Keep notebook protocols unconsolidated so dynamically defined resources use their custom display without consolidation warnings.

1.3.0

Choose a tag to compare

@palermo-git palermo-git released this 06 Oct 22:44

Historical release record for the existing v1.3.0 tag, dated October 5, 2026 (commit a9f98095d632a28aa4861a5fc4154a24451367c0). This GitHub record was reconciled on October 6, 2026; the original tag and package are unchanged. The notes below include the subsequent correction to the delivery guarantee. See current releases for upgrade guidance.

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_attribute on the outbound_delivery section. The exact
    payload-bytes column's name is configurable (default :payload):
    rename it when the consumer's domain reserves payload for its own
    sole payload store. The injected column, the :dispatch accept list,
    and every signing/sending read follow the configured name; a name
    colliding with another injected field fails closed at compile.
  • prune_action :none on the outbound_delivery section. Opt out
    of the retention destroy :prune action's injection for append-only
    audit ledgers (no destroy action exists on the resource at all);
    AshHooks.Delivery.prune/2 then fails loud with a named error —
    deletion is the consumer's own surface. Default :destroy is
    unchanged.
  • status_attribute / enabled_values / disabled_value on the new
    endpoint section.
    Map the durable enable/disable onto the
    consumer's OWN switch (e.g. an active boolean): status is 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 through AshHooks.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 (postgres job; the suite is env-gated by ASH_HOOKS_POSTGRES=1
    and excluded from ordinary dev runs). ash_postgres is a dev/test-only
    dependency — never ships, never constrains consumers.

Fixed

  • :dispatch/:ingest no longer accept :id on resources whose
    primary key is a non-writable :id
    (e.g. uuid_v7_primary_key):
    Ash's ValidateAccept raised at compile on exactly that consumer
    shape, making the extensions unadoptable without replacing the PK.
    The accept lists carry :id only 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-:id primary 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 the mark_sending CAS and
    inbound claim fence.
    Correction 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 custom 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 :disabled dead-lettered, so a
    consumer-redeclared status with extra values delivered). The
    dispatcher's skip and the send path now route through the ONE
    AshHooks.Endpoint.enabled?/1 check — fail-closed by design.
  • AshHooks.Subscription.matches?/2 normalizes event_types entries
    with to_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 redeclaring
    event_types keep 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 --check ran without an extension list
    and checked nothing; it now passes the five extensions explicitly, so
    a stale documentation/dsls/ actually fails the build (the sheets in
    this release are regenerated under that invocation).
  • Dev/test mint bumped 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_postgres 2.13.1 is added dev/test-only for the
    Postgres leg above.

Docs

  • Policy obligation at the extension site (OutboundDelivery,
    Endpoint moduledocs): 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.id guidance (Event, Dispatcher
    moduledocs): 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.Http moduledoc): headers
    must be a LIST of {name, value} tuples (a map silently loses
    Retry-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_seconds semantics (Worker moduledoc): 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 (Delivery moduledoc): 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 — documentation tree ships in the tarball

Choose a tag to compare

@palermo-git palermo-git released this 24 Sep 22:01

Packaging fix, no library code change.

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.

v1.2.0 — multi-tenancy (attribute-aligned, fail-closed)

Choose a tag to compare

@palermo-git palermo-git released this 24 Sep 13:52

Packaging follow-up in 1.2.1: the documentation tree (including this release's tenancy adoption checklist) now ships in the hex tarball.

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 (:tenant
    opt), AshHooks.Ingress.ingest/4 (ctx[:tenant]), the claim/mark/
    renew/redact/prune/reap heads (:tenant opt, new optional arity),
    AshHooks.Delivery.run/2 (tenant recovered from job args), and both
    prunes. The tenancy contract: consumers declare
    multitenancy :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?: true is rejected — it
    silently disables fail-closed reads). A compile-time verifier
    additionally rejects multitenancy :bypass, :bypass_all, and
    :allow_global actions on multitenant package resources.
  • Per-tenant operations — AshHooks.reap_all/2 and
    AshHooks.prune_all/2 sweep 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 :pending by 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-arity f(ref, tenant) resolver contract);
    the inbound secret fn tenant -> ... end source; the provider
    behaviour's optional webhook_signing_secret/2
    (Organization × connection custody; use AshHooks.Provider ships an
    overridable default delegating to /1); and :tenant in the
    verify_signature/3 context map.
  • Oban args carry the tenant — the generated enqueue/2 serializes
    the row's tenant (string tenants are the supported shape; non-string
    tenants must round-trip parse_attribute over 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
    :reconciled status.
  • 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.

v1.1.1

Choose a tag to compare

@palermo-git palermo-git released this 17 Sep 03:39

1.1.1 — 2026-09-16

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 :inets httpd fixtures hardcoded server_root: "/tmp",
      nonexistent on Windows — one failure hid as a setup_all
      invalidation that silently excludes a whole module's tests from
      the failed count; both now use System.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).

v1.1.0

Choose a tag to compare

@palermo-git palermo-git released this 16 Sep 23:03

1.1.0 — 2026-09-16

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.0 pin 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-versions and 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.exs refuses any OTP release CI
    does not test (currently 28/29) before anything compiles, because
    System.version/0 does 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.audit now runs in CI on every
    push.
  • Dependencies moved to latest (mix hex.outdated shows 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.audit clean before and after (and OSV shows
    no advisories against the three new versions).

Fixed

  • A %Regex{} inside a module attribute (@redaction_patterns) embeds
    the compiled re_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 a defp (identical regexes, flags, and
    order); with the floor now at 1.20 that combination is out-of-window,
    and the defp keeps the escape class dead if the floor ever drops
    below 1.19.
  • mix dialyzer on a fresh PLT no longer reports 13 unknown-function/
    unknown-type warnings in test/support: use AshSqlite.Repo emits
    Ecto.Adapters.SQL delegations, and the deps-PLT app enumeration does
    not reliably include :ecto_sql — it is now explicit in
    plt_add_apps with the reason inline.

Added

  • scripts/check-currency.sh: exits nonzero whenever mix hex.outdated
    shows 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 in mix.exs. Fail-closed: an
    errored hex.outdated run (lock mismatch, resolver failure) also exits
    nonzero.

v1.0.4 — current-Ash compatibility + byte-bound corrections

Choose a tag to compare

@palermo-git palermo-git released this 16 Sep 11:09

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

  • 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 on lock-free resolution. The test application (repo-local, never shipped), the get-started Livebook, and the tutorial now carry the setting; the package writes no :ash configuration — consumers keep that choice, and both documented values work (see UPGRADING.md). A tripwire test locks the boundary.
  • Byte-bound corrections (each red-proven):
    • Captured snippets and error summaries are capped in BYTES instead of grapheme-sliced — under :codepoints a grapheme-sliced 2048-char snippet of combining characters spans 4000+ codepoints and crashed the post-send ledger write (the same re-send poison class the control-byte strip closes).
    • Event.new/1 and the dispatcher guards bound ids/types by bytes — a 128-grapheme combining type previously passed validation and then failed every endpoint's dispatch create under :codepoints.
    • A thrown 255-char token no longer overflows last_error (262 bytes failed the enqueue-failure write itself).
    • An inbound handler failing with an invalid-UTF-8 binary now records [binary] instead of losing the failure record and leaving the delivery re-drivable.
  • The tutorial's requirements line matches the tested floor (Elixir ~> 1.17, ADR-0010).

Changed

  • mix.lock: ash 3.33.4; dev/test substrate cleared of published advisories (ash_sqlite 0.2.19 / ash_sql 0.7.5 — test-only; mix hex.audit clean).

Full notes: CHANGELOG · UPGRADING · hex.pm

v1.0.3 — docs-currency release

Choose a tag to compare

@palermo-git palermo-git released this 23 Aug 03:52

No functional changes — 1.0.2's coverage gate and fixes unchanged. This release closes the audit's currency gaps:

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. CI uploads the cover HTML when the coverage gate reds.

Docs

  • UPGRADING now carries the 1.0.2 behavior notes: the :httpc literal-IP HTTPS refusal (:ip_literal_https_needs_bounded — use the default adapter for literal-IP HTTPS) and the :http_opts threading fix.
  • The :httpc and Bounded moduledocs document the literal-IP posture and the :cacerts trust-store seam; ADR-0009 records the seam decision in Consequences.

v1.0.2 — the 100% coverage release

Choose a tag to compare

@palermo-git palermo-git released this 23 Aug 02:08

The maintainership directive superseding 1.0.1's print-only coverage posture, worked through the wayfinder decisions (#20-#22): line coverage of lib/ now gates CI at 100% — every runtime-reachable line covered, 31 provably-dead defensive arms deleted with proofs rather than exempted, and the ignore list carrying only what :cover structurally cannot observe (Spark's generated DSL sections, test-support, four compile-window module-body lines).

Added

  • Injectable :cacerts trust bundle on both HTTP adapters (Target.ssl_options/2; worker http_opts — compile-time literals or {m, f, a} resolved per-perform) for private-CA endpoints. Default unchanged (OTP CA store).
  • TLS success paths covered by real CA-verified loopback sessions against committed local fixtures, including the literal-IP iPAddress-SAN floor.

Fixed

  • CertSan.ip_san_match?/2 fails closed (returns false) instead of crashing its caller on garbage SAN bytes — asn1 decode failures exit on OTP 29 and raise on 27/28; the bare catch covers every class (cross-vendor reviewed).
  • The alternate :httpc adapter refuses literal-IP HTTPS fail-closed (:ip_literal_https_needs_bounded): it cannot run the IP-SAN floor, and chain validation alone would let any chain-valid cert authenticate the endpoint IP. Use the default adapter there.
  • use AshHooks.Worker no longer silently drops :http_opts.

Send failures now classify through the read path — the Erlang inet/ssl drivers queue sends and surface peer-death errors on the next operation (probed on both platform families, both transports); the unreachable {:send_failed, _} wrap is gone with the probe evidence in the commit messages.