All notable changes to this project are documented in this file.
Unreleased
v1.5.0 — October 7, 2026
Security dependency release. The package DSL, public API, database schema, persisted response format, and token wire format are unchanged; no migration is required.
Breaking dependency change: consumers pinned below Ash 3.34.3 or Mint 1.10.2 must update those dependencies. See Upgrading for the steps.
Security
- Ash floor raised to
>= 3.34.3 and < 4.0.0. EEF-CVE-2026-94201 affects Ash>= 3.5.1 and < 3.34.3through unsafe atom-attribute filters that can exhaust the BEAM atom table. Ash 3.33.11 includes the EEF-CVE-2026-93477 fix but remains affected by EEF-CVE-2026-94201, so 3.33.11 cannot be the package floor. - Optional Mint floor raised to
~> 1.10 and >= 1.10.2. Mint 1.10.2 backports the fixes for EEF-CVE-2026-91043, EEF-CVE-2026-92103, and EEF-CVE-2026-94194. Mint remains optional and runtime-excluded; the requirement binds only dependency graphs that already carry the installer HTTP closure. - Security patches for existing feature lines: 1.4.2 and 1.3.4 carry the same dependency floors. Applications staying on those lines can take the patch without adopting a newer feature line. Neither patch changes its line's DSL, API, schema, or wire format.
Changed
Result-handling examples preserve unhandled typed errors for the application’s handler; release checks exercise every documented code against all four introductory expressions.
Lock:
ash3.33.11 → 3.34.4,ash_postgres2.13.1 → 2.14.3,ash_sql0.7.6 → 0.8.1,spark2.7.3 → 2.7.6,mint1.10.1 → 1.11.0, withex_ast0.16.0,finch0.24.0, andreq0.7.5 riding the resolution, for the dependency-currency gate. The development lock exercises current releases while the published ranges retain the patched Ash 3.x and Mint 1.x compatibility lines.Actual Livebook source execution is now a release gate.
elixir scripts/check_livebooks.exs --localevaluates every Elixir cell and persisted output in all three.livemdfiles against PostgreSQL before packaging; the--publishedmode repeats the same notebooks from the Hex dependency after publication. The existingLivebookWalkthroughTestremains a supplemental mirror test for selected idempotency and nonce shapes. The v0.1.1 entry below records the historical claim that this mirror regression-pinned the notebooks; it did not read or execute notebook source, and this release corrects that verification boundary.Developer database setup follows the current repository policy. The repository no longer ships
docker-compose.yml; contributors point.envat PostgreSQL 18 and source it before the gate battery. macOS and Linux are the supported developer platforms; Windows developers use WSL2, and CI runs on Linux only. This current guidance supersedes the v1.3.1 historical repository description without rewriting that release record.
v1.4.0 — 2026-09-24
Breaking behavior change (the reason this is a minor, not a patch): a concurrent
same-key retry of an external-effect action that previously waited out the original and
replayed may now return :request_in_progress within the configured wait. Clients already
treat that code as wait-or-retry; consumers running check-then-act peers are the direct
beneficiaries.
Added
- Pre-peer claim lock for external effects (ADR-0010). Before any peer call, the
package takes the claim row
FOR UPDATEin the caller's open transaction with a bounded wait — the new per-protectionexternal_lock_timeout_msoption (default 2000, validated1..25_000— 5s of headroom under the committed-claim worker kill, requiresexternal_effect). The same-key retry's committed-claim worker arms the same bound. A lock timeout maps to:request_in_progress(the local path's existing concurrent-retry semantic) and is terminal. The lock is generation-safe: it binds the claim id alongside the logical key, so a reaped-and- reinserted row fails closed (:store_invariant) instead of being adopted. Motivation, observed on live PostgreSQL: without the lock, an honest in-flight retry truthfully recovered:absentand executed under the same operation key while the original's execute was still in flight — and a check-then-act peer (conforming to the contract as published at v1.3.2) double-spends in exactly that window. With the lock, two executes under one key cannot overlap at the peer; dead-caller recovery is unchanged (test/ash_onetime/external_contention_test.exs,test/ash_onetime/store/contention_test.exs). The mutation battery pins the lock twice:effect-lock-generation(id bind defeated while kept, reds on generation adoption) andpre-peer-lock-removed(the lock call removed, reds on the unlocked race — B's red-proof). Thelock_timeoutGUC is scoped to the lock: captured before, restored after — it never caps the adapter's own writes. The internal AshOnetime.Store growslock_for_effect/3and an options-takingclaim_committed/3(the 2-arity facade is unchanged).
Decisions (ADRs 0009-0011, each adversarially reviewed and judged)
- ADR-0009 (Rejected): reap-surviving derived operation key. A stateless derivation survives retention too — a post-retention retry would replay the prior generation's peer result instead of executing (a disclosed duplicate traded for an undisclosed no-op); a stateful one is correct only under an unknowable peer key-retention horizon; and the operation key is published callback API, so a derived key breaks every conforming adapter's claim join. The operator-side defense stays: business-level reconciliation before enabling the reaper on external-effect actions.
- ADR-0011 (Rejected for wall-clock): library-side callback bounding. The only
in-package bounding mechanism (
timed_callback/4's Task) runs the callback outside the caller's transaction, silently breaking transactional adapter writes. The DB-resource bound (opt-inidle_in_transaction_session_timeout) and a configurable immediate post-ambiguousrecover/3are recorded as candidates with named decision criteria.
Documentation
- External-effect contract corrected (design review 2026-09-24; decision records under
.kimosabe/specs/):- ADR 0001's claim that a duplicate side effect requires BOTH a lying adapter AND a
failing peer is corrected by a dated amendment: an honest in-flight retry alone
produced concurrent same-key executes (now prevented by the lock; the peer-side
residue — a sequential lying
:absent— keeps key idempotency a MUST, with atomic key claims a SHOULD). - The adapter execution environment is normative: both effect callbacks run inside the
caller's open PostgreSQL transaction (observed per callback, including the caller's
backend
idle in transactiononpg_stat_activity) and the package applies no timeout to them; adapters MUST bound their own peer call, and an ambiguous execute is followed by recover in the same transaction (up to two adapter timeouts of hold). - Reaping an external-effect claim documented honestly: the operation key is the claim UUID, so a post-reap retry runs under a new key and no key-based defense remains at either layer (external-effects guide, operations guide, ADR 0002 amendment).
- New
:absentvs:unknownmapping table for real HTTP peers (the 404-under-replica-lag double-spend trap) plus a worked bounded HTTP adapter in the external-effects guide. - New Recipe 4 — external effect via a transactional outbox (the outbox row as the
ExternalEffect peer, keyed by the operation key, committing with the action), including
the acceptance-receipt classifier rule, the race-absorbing
on_conflictkey claim, and the receiver-side cross-generation deduplication boundary. :request_in_progressdocumented on the external path; the previously unlisted:worker_timeoutadded to the error table.
- ADR 0001's claim that a duplicate side effect requires BOTH a lying adapter AND a
failing peer is corrected by a dated amendment: an honest in-flight retry alone
produced concurrent same-key executes (now prevented by the lock; the peer-side
residue — a sequential lying
v1.3.2 — 2026-09-20
Security
Optional Mint floor raised to
~> 1.10 and >= 1.10.1following ADR 0004:mix hex.auditflagged EEF-CVE-2026-82672 against the locked 1.10.0 — an unvalidated chunk-size line tail in Mint's HTTP/1 client enabled response smuggling against strict intermediaries on pooled connections. 1.10.1 (2026-09-19) carries the fix. Mint staysoptional: trueand runtime-excluded: the floor binds only hosts that already carry the installer closure, and no core consumer pulls it. The package check, optional-integration matrix, and dependency-security tripwires move together. OBSERVED:mix hex.auditclean after the raise.Lock:
ash3.33.4 → 3.33.8 andash_sql0.7.5 → 0.7.6, riding the dependency-currency gate (resolver-updatable drift). The published non-Mint floors are unchanged —>= 3.33.4 and < 4.0.0for Ash remains the CVE-derived minimum;mix hex.auditreports no advisories against the new lock.
Added
AshOnetime.Verified.new/1— the sanctioned constructor for the opaqueVerifiedtype.tstays opaque — a Dialyzer-enforced construction convention: consumer code that builds the struct literal violates the opacity contract (contract_with_opaque, OBSERVED at 1.2.3 in a consumer sweep; suppressed there pending this release). Opacity is not the runtime anti-forgery boundary — that remains the admission path (DSL-configured verifier/minter callbacks under a bounded context, reserved-input rejection).new/1validates at mint time what every internal consumer holds aVerifiedto:keyandverifier_idnon-empty binaries (verifier_idat most 128 bytes),issued_atand the optionalexpires_atwell-formed DateTimes withexpires_atat or afterissued_at; unknown, duplicate, and missing keys are rejected, and the constructor never raises. Purely additive: no existing export changed (the export census grewnew: 1), the Elixir requirement and non-Mint dependency floors are unchanged. The nonce livebook, its walkthrough mirror, and the transaction-owned admission guide now demonstrate the constructor; CI gained a test-env dialyzer leg so the consumer-shaped opacity fixture stays analyzed.
v1.3.1 — 2026-09-17
No consumer-facing changes: the published Elixir requirement (~> 1.20), every
dependency floor, and the public API are identical to v1.3.0. This release hardens
how the project itself builds and verifies — on every platform it supports.
Changed
- Toolchain identity is enforced in-repo, before anything compiles. The Elixir
support window stays
~> 1.20;config/config.exsnow refuses any Erlang/OTP build outside the supported set (28,29).System.version/0does not encode the OTP build, so a same-Elixir foreign build previously compiled silently and poisoned shared_build/PLT state. The guard never ships —config/is excluded from the package — and its set grows only in the same commit that adds a CI leg: a dedicatedotp-28CI job now proves the 28 end, while.tool-versions(Elixir 1.20.4 on OTP 29.0.3) and the primary CI runtime hold the 29 end. - macOS/Linux/Windows capability is a hard requirement, not a preference. Every
POSIX-only tool in the developer surface is gone: the dependency-currency gate
moved from a bash script to
scripts/check_deps_currency.exs(same exit-code contract; all four outcomes proven — green, resolvable drift, deliberate pin, unparsable table), everySystem.cmd("mix", ...)call site in tests and battery scripts spawns throughAshOnetime.Portable(cmd /cwrapping for Windows' .cmd shims), and.gitattributes(* text=auto eol=lf) keeps a Windows checkout byte-exact formix format --check-formatted. CI proves the weakest platform continuously: awindows-buildlane (clone, deps, format, compile, audits, credo, currency gate, hex.build) and awindows-db-batterylane that installs PostgreSQL 18 natively via chocolatey (service containers are a Linux-only feature) and runs the fullmix test. The heavier release gates (mutation battery, unpacked-package check, optional-integration matrix) remain ubuntu-proven and are named as such in the workflow. - Test database substrate:
docker compose up -dprovisions the exact PostgreSQL 18 the harness requires, with the host port taken from.env(PGPORT, default 18841 — the same port CI pins) so the dedicated instance never collides with another local listener;.env.exampledocuments thePG*variables, the fail-closedDATABASE_URL, andHEX_API_KEY. The harness still fails closed — on the dedicated shape rather than one string: ecto scheme,postgresuser, loopback host,ash_onetime_testname, any port except the shared default 5432. - Dependency currency is mechanical:
scripts/check_deps_currency.exsexits nonzero on resolver-updatable drift (mix hex.outdated"Update possible") and prints blocked packages with the requirement chains holding them. OBSERVED: dialyxir 1.4.7 → 1.4.8, ex_doc 0.40.3 → 0.40.4, and oban 2.23.1 → 2.24.1 moved to latest;mix hex.auditreported "No retired or security advisory packages found" before and after the move.
v1.3.0 — 2026-09-16
Security
- Breaking dependency change: raise the consumer floors to Ash 3.33.4,
AshPostgres
~> 2.13, AshSql 0.7.1, Igniter 0.8.4, and Mint 1.10.0, following ADR 0004. OBSERVED:mix hex.auditon September 15, 2026 reported 26 advisories against the previous lock; the OSV ranges and Hex release metadata identify these patched minimums. EEF-CVE-2026-86338 (published September 16, fixed only in Ash 3.33.4) moved the Ash floor above the initial 3.33.0 choice before release — CI's floor-cell audit caught it. The doctor, CI floor, and package check move together. Mint is optional and excluded from this package's runtime application list; existing optional integrations retain their startup boundaries. Ash 3.33 additionally requires the consumer configconfig :ash, default_string_length_count: :codepoints(or:mixed) — without it an application's own resources fail to compile; see the upgrading guide for the full consumer steps, including this one. - Lock:
ash3.32.0 → 3.33.4,ash_postgres2.11.0 → 2.13.1,ash_sql0.6.6 → 0.7.5,igniter0.8.3 → 0.8.4,mint1.9.3 → 1.10.0, withreq0.7.4,spark2.7.3, andspitfire0.4.1 riding the resolution.
Changed
- Test harness: Ash 3.33's mandatory string-length counting mode is set to the
recommended
:codepoints(config/config.exs), and the compile-fixture subprocess runners start Mix and mirror the mode (the fresh VMs read no config). The dynamic-repo transaction test's readiness receives and the test pool's checkout queue were widened to bounded patient windows — the default 100ms/6s ceilings flaked under scheduler and host load; no fail-closed semantics changed. The optional-integration matrix script now also observes the resolved Igniter and Mint versions against the security floors, gains conflict proofs (exact advised pins must fail to resolve), and joins the release-gate battery.
v1.2.3 — 2026-08-27
Changed
- Runtime application closure of the package's
.appspec —plug,oban,igniter, andstream_datanow carryruntime: false, so none of them appear in the generatedapplicationslist anymore. Before this release, every host that carried one of the optional integrations in its own dependency closure inherited it as a runtime application of ash_onetime (the extension's app spec claimed its boot and release closure), and every host inheritedstream_dataunconditionally. The opt-in contract is unchanged: the Plug module, the Oban workers, and the Igniter installer compile exactly when the host itself lists the dependency (the optional-dependency matrix passes unchanged), and the published Hex requirements still mark the three integrationsoptional: true.stream_dataremains a published requirement because Ash itself requires it transitively in every environment — Mix rejects anonly: :testrestriction alongside Ash's unrestricted entry — but it no longer contributes to this package's runtime applications. Dev-side, the dialyzer PLT now names:oban,:plug, and:igniterexplicitly, sinceruntime: falsedeps are no longer seeded into the PLT automatically while the guarded integration modules still analyze against them.
v1.2.2 — 2026-08-25
Added
AshOnetime.Transaction.claim_id/1— the one sanctioned accessor for a fresh admission's claim UUID (Admissionstays opaque by contract). The UUID is the durable address of the claim inside the store: persisting it lets a host correlate its own invocation records with the claim and hand the unchanged UUID to an external execute/recover peer. A pure read (no process/transaction/store interaction); test-pinned against the stored claim row, and the architecture export inventory is updated.
v1.2.1 — 2026-08-22
Changed
- README: a mutation-battery badge (130 sentinels, each proven RED under its own mutation before it is trusted) and an explicit test-assurance statement — every library module at 100% line coverage, with the mutation battery as the assurance bar. Documentation only; no code, DSL, or contract change.
v1.2.0 — 2026-08-22
Additive operations-preflight and hardening release. No breaking change, no DSL/contract change, no migration.
Added
mix ash_onetime.doctor --live— opt-in schema-currency preflight: connects to the database read-only and fails when the installed schema is not current for the running package (missinglogical_partitioncolumns, response-payloads table or its_defaultpartition, cleanup/reap functions by exact arity, or delete-guard triggers) — the upgrade-without-migrating failure mode, caught at preflight instead of at the first admission after deploy.- Backup/restore runbook (
documentation/operations.md) — therestore-from-backupprocedure: per-strategy exposure of a rewound admission store (bounded nonce windows, fresh re-execution of post-restore-point idempotent effects), reconciliation scoping, and the post-restore partition roll-forward. - PostgreSQL floor documentation (README, operations.md) — the SQL surface requires
PostgreSQL 11+ (declarative hash/range partitioning with default partitions,
SKIP LOCKED,pg_advisory_xact_lock(bigint)); CI exercises PostgreSQL 18 and versions below it are unverified.
Fixed
- Store digest comparisons unified to constant-time — the last two plain-
==digest comparisons in the store (the stored-payload digest check on replay load and the caller-digest check at completion) now use a size-guarded:crypto.hash_equalshelper, one convention for every digest comparison site in the library. No behavior change: neither site compares attacker-secret material, and both operands were already 32-byte-constrained. - Test suite: the
RollContentionTestfull-suite flake (~10%/run) is eliminated at the mechanism —Sandbox.start_owner!/2's asynchronous teardown could leave the caller's ownership allowance in place long enough for a back-to-back second owner in the same process to crash with{:already, :allowed}. Same-process sequential owner sites now use an in-processcheckout/checkinhelper (AshOnetime.Test.RealConnection), with a 100-cycle tripwire pinning the class; the timing-dependent telemetry-flush and ETS-TTL sleeps are deterministic. No library code changed. - Test suite: direct unit coverage for the admission decision functions — every
resolve/5arm (allexecution_class/2mappings, transaction-mode/locator/identity/ claim-state invariant rejects,:completereplay and fingerprint mismatch,:processingper mode,:collisionmatch/malformed, the exact-shape:execute_untrackedescape with a fail-closed test per guard dimension, thestore_errorfallthrough) plus the request/claim sanitizers, exposed as@doc falsetest seams and pinned by the architecture census. The dev-build seam-absence tripwire was also de-vacuated: its subprocess probe now loads the module beforefunction_exported?/3(an unloaded module probes false unconditionally).
v1.1.0 — 2026-08-20
Additive transaction-owned admission for applications whose effect boundary is an existing Ecto transaction rather than an Ash action.
Added
AshOnetime.Transaction— publicidempotency/2,nonce/2, andcomplete/2operations that require an existing PostgreSQLREAD COMMITTEDtransaction and never start or commit one. Idempotency replays exact response bytes; nonce collisions reject; typed errors and all claims remain bound to the caller transaction.- Logical partitions — a bounded UTF-8 authority component included in every claim, collision identity, response payload locator, cleanup path, and cache key. Identical operation/scope/key triples in distinct partitions are independent without importing a host application's tenant types.
mix ash_onetime.gen.logical_partitions— reversible upgrade migration for 1.0 installs. Existing rows backfill toglobal; rollback refuses while non-global authority exists.
Compatibility
- Existing Ash resource protections continue in the
globallogical partition and retain their existing public behavior. - Fresh generated installs include logical partitions. Existing installs run the new upgrade task once before admitting non-global transaction-owned work.
Documentation
- Updated Getting Started, Upgrading, README status, and every runnable Livebook to select the
1.1package line. All three notebooks now link the transaction-owned admission guide, and a documentation-currency tripwire keeps their install requirements aligned withmix.exs.
v1.0.0 — 2026-08-19
The 1.0 stability release: the surface contract is frozen (published in full in upgrading.md — docs are the contract; breaking changes to the documented surface now happen only at 2.0, with reserved break-rights for security, behavior-correcting bug fixes, and new compiler/linter warnings). Pre-1.0 (0.x) releases were explicitly non-binding; 1.0.0 is the compatibility baseline. The persisted response format (ADR-0007) and the token wire format (ADR-0008, window-bounded) are frozen compatibility surfaces.
Fixed
- Ash requirement pin validation (release tooling):
ASH_ONETIME_ASH_VERSIONis now compile-time validated — it must parse as a version AND lie within the published requirement range; pre-release and build suffixes are rejected; a release exported with the variable unset is asserted by the package gate to carry the floating requirement in the archive metadata. - Telemetry contract (
AshOnetime.Telemetry): the defaultattach/0router now covers the full closed surface — all 13 events including the:uncertain_exceptiondiagnosis event (previously silently dropped); the moduledoc was rewritten to the router reality (the phantommetrics/0reference is gone); the diagnosis event's closed atoms-only metadata shape (%{strategy, phase, exception}) is validated before emission. - Admission test seam: the Process-dictionary test-store redirect in the internal
Admission module is no longer compiled under
Mix.env() == :test— it is gated onApplication.compile_env(:ash_onetime, :allow_admission_override, false), default-off in every build environment (mirroring the token verify-clock gate). A consumer building deps withMIX_ENV=testno longer ships the redirect; calling the seam without the explicit opt-in now raises a named error.
Documentation
- ADR-0008 records the token wire-format compatibility rule (window-bounded: a 1.x reader verifies tokens minted by any earlier 1.x writer while inside their acceptance window; no permanent freeze) alongside the persisted response-format rules (ADR-0007).
- The operations runbooks' diagnostics are all executable (each snippet verified against a live database): the vacuous default-partition count became a stranded-row count, the non-runnable IEx backlog snippet was replaced by its SQL path, and the pool diagnostic is a checkout-queue watch on the repo query event.
- Worker-backoff records corrected (ADR-0005 and every prose carrier): Oban's default is ~36–40 s at three attempts, not hours; the custom backoff's rationale is re-derived honestly (spaced DDL-class retries, wide jitter, 120 s cap).
- ADR batch: ADR-0001 (external-recovery
:absenttrust boundary), ADR-0003 (live committed-claim failure taxonomy), ADR-0004 (forward-compatible floor posture) amended; ADR-0006 (point-events observability) and ADR-0007 (persisted response-format compatibility for 1.x) added. - SECURITY.md publishes the support and disclosure policy (supported-versions table, 7-day ack / weekly updates / coordinated disclosure); CI battery claims name the per-cell battery precisely; the ROADMAP is reconciled (portable status column, all eleven rows verified).
v0.7.0 — 2026-08-18
Security release: raises the Ash floor to 3.31.3 after EEF-CVE-2026-67579. Breaking dependency change — consumers on Ash 3.29.3–3.31.2 must bump Ash to ≥ 3.31.3 (the library is pre-1.0, so the minor version carries the break per the documented convention).
- Ash floor raised to 3.31.3 (from 3.31.1). EEF-CVE-2026-67579 (filter expression
injection via a forged keyset pagination cursor — HIGH, CVSS 7.5, unauthenticated, no
application-side workaround) affects Ash below 3.31.3 and is fixed only in 3.31.3; Hex
additionally retired 3.31.1. The published
>= 3.31.1requirement let consumers resolve a retired or advised Ash, andmix hex.audit— a required gate — failed on the 3.31.2 lock. The floor was derived from the complete OSV advisory inventory for hexash(11 advisories; maximum fixed-version 3.31.3; 3.31.3 not retired), not from the absence of a Hex label. See ADR-0004 (Security-driven Ash floor, amended). The CI matrix moves from[3.31.1, latest]to[3.31.3, latest]— transiently both cells resolve to 3.31.3 until the next Ash release. - Doctor floor guard directly tested: the doctor's Ash-floor verdict is extracted as a
pure
floor_status/1seam with reject tests for the retired 3.31.1 and the advised 3.31.2 (red-proven before the floor bump), plus a mutation-battery entry (doctor-ash-floor) that re-proves the guard red-capable on every release run. The Oban queue advisory verdict is likewise a pure seam (oban_queue_status/2) with both arms — Oban loaded and not loaded — covered. Doctor output is unchanged; the three strict-Credo findings that red-barred the latest-Ash CI cell are fixed. - Lock:
ash3.31.2 → 3.31.3 (withecto3.14.2 andreactor1.0.6 riding the resolution).
v0.6.0 — 2026-08-10
Minor bump: two additive enhancements (new Mix task + Phoenix guide) plus internal perf/cleanliness gap closures. No breaking change, no consumer upgrade action.
mix ash_onetime.doctor— a read-only upgrade-preflight that checks the Ash security floor (fatal if below 3.31.1), Oban queue configuration (advisory — warns on a missing:ash_onetime_partitionsqueue that strands the retention-safety path), and prefix validity. Mirrors the runtime Mix task family; run after install and on each upgrade.- Phoenix integration guide (
documentation/phoenix.md) — a runnable Phoenix controller recipe wiring the Plug, thereplayed?/1→ 200/201 +Idempotent-Replayedheader signal, and the error-code → HTTP-status mapping into a complete controller pattern. Closes the gap where every existing recipe stopped at plain Elixir tuples. - Cleanup delete-guard probe partition-scoped — the
:complete-branch probe in the install migration now constrainspartition_date = OLD.response_partition(the H1 read-path tail), turning an O(partitions) scan into a point lookup on every completed-claim delete. Same property shift as H1 (partition pruning vs cross-partition detection are mutually exclusive); the write path remains the authoritative guard. - Dead
trusted_contextparameter removed from the internal scope/key resolution path (admission.exprepare_resolved/scope_hash/scope_component/key_hash/key_component). No behavior change — the parameter was threaded but never read after M1's collapse. - L4 monitoring blind spot closed — a stuck-
availablePartitionWorker query added tooperations.md(the discard-alert watcheddiscardedonly; an unconfigured queue leaves jobs inavailableforever with no signal).
v0.5.1 — 2026-08-10
Patch: test-only hardening. No consumer-visible change; no upgrade action required.
- Invariant tripwire hardening (test-only): the five test-coverage residuals the v0.5.0
closeout accepted as documented gaps are closed. Each tripwire now observes the production
function it claims to pin, and every one is proven RED-capable by mutation (mutate the
production line, watch the test fail). The Oban worker error-tuple tests force a REAL
:lock_timeoutthrough database lock contention rather than reading source text. No production behavior change. - Internal test seams: three pure helpers are now
@doc falsepublic callables so their contracts can be pinned directly —Store.Postgres.roll_advisory_key/1,Cache.key/1,Resource.Verifier.verify_required_shape/2. Undocumented, not a supported API. - Lint:
mix credo --strictnow passes clean (implicit-tryandcond→ifrefactors in the ETS cache adapter and the telemetry default handler — behavior-identical). - Dependency: bumped
ashfrom the retired3.31.1to3.31.2(within the existing>= 3.31.1 and < 4.0.0pin). 3.31.1 was retired upstream for a breaking bug.
v0.5.0 — 2026-08-10
Minor bump: sixteen findings (M1–M5, L1–L11) from the v0.4.0 independent code review. Four
are CONSUMER-VISIBLE (read documentation/upgrading.md before upgrading); the rest are
internal hardening.
Independent code-review fixes (M1–M5, L1–L11): sixteen findings from the v0.4.0 independent code review, landed in four surface-cohesive clusters:
- M1 — bounded callback context: verifier/mint/scope callbacks now receive exactly
%{resource:, action:}(the prior code ran a deadMap.take(trusted_context, [:keys, :now])and threaded caller actor/tenant that no callback read; both dead paths removed). Least-privilege contract pinned by an admission test. - M2 — reserved-input compile check now matches the runtime guard (CONSUMER-VISIBLE): a protected
resource declaring a reserved-named attribute (
:key/:issued_at/...) with no accept now fails to compile (previously caught only at runtime byreject_reserved/1). Rename any such attribute (e.g.:idempotency_key). - M3 —
@reservedsingle-source: the reserved-input list is nowAshOnetime.reserved_verification_inputs/0, shared by the transformer (compile-time) andAdmission.reject_reserved/1(runtime); the two copies can no longer drift. - M4 — verifier defense-in-depth: the Spark verifier now re-asserts strategy in
[:idempotency, :one_time_nonce], non-nil scope, non-nil key (mirroring the transformer), catching a transformer regression before runtime. - M5 — clock override gate:
@allow_test_clock_override(Mix.env() == :test, deployment-fragile) replaced byApplication.compile_env(:ash_onetime, :allow_clock_override, false)— the verify-side:clockoverride is off by default in every build; the test suite opts in viaconfig/test.exs. - L1 — constant-time comparator consolidated onto
:crypto.hash_equals/2(the hand-rolled XOR-reduce is gone); theverify/3rescue is documented as load-bearing for wrong-length signatures (hash_equals/2raises on unequal length — verified empirically). - L2 — reap floor enforced at the guard:
reap/3rejects sub-86_400 callers with:invalid_request(no DB round-trip) instead of the migration's22023misclassified as:store_invariant. - L3 — per-tenant advisory lock: the partition-roll lock key is derived per-prefix, so
distinct tenants roll concurrently (within-tenant serialization preserved). Cannot
reintroduce a
CREATE PARTITION OFrace (per-schema partitions, distinct OIDs). - L4 — dedicated
:ash_onetime_partitionsOban queue (CONSUMER-VISIBLE):PartitionWorkermoves off the shared cleanup queue so forward partition creation (retention-safety) does not compete with routine cleanup under saturation. Operators must configure the queue.documentation/operations.mdupdated. - L5 — worker error tuples carry the inner reason (CONSUMER-VISIBLE):
{:error, :tag}→{:error, {:tag, reason}}so the distinguishable store cause survives Oban exhaustion. Consumers pattern-matching the old bare atom must update to the 2-tuple inner shape. - L6 — store-transaction exception telemetry:
committed_claim_transactionrescue emits[:ash_onetime, :uncertain_exception]with the exception class before collapsing to:dispatched_unknown. Telemetry-only posture preserved (no Logger). - L7 — cache key length-prefix: defense-in-depth framing for the cache key (today's fixed-32-byte components have no ambiguity; the framing future-proofs against a variable-length change).
- L8 — roll_forward OID scoping: the
_defaultpartition OID subquery is now namespace-scoped (pg_namespace), matching the schema-scoped existence checks. - L9 — change/generic_action dedup:
dispatch_reservation/trusted_context/unavailable_errorhoisted toAdmission(the near-verbatim duplicates removed). - L10 — ensure_callbacks cycle diagnostic:
Code.ensure_compilednow distinguishes the:unavailable/:module_unavailablecycle case with clear ordering guidance. - L11 —
prepare/3spec dead arm removed (theResult.t()arm never matched); dialyzer confirmed the downstreamstore_error/1was dead code and it was removed.
- M1 — bounded callback context: verifier/mint/scope callbacks now receive exactly
Replay read prunes to the claim's payload partition (H1): the idempotency replay read path (
load_payload/2) now constrains itsash_onetime_response_payloadsquery bypartition_dateas well asclaim_id, turning a replay of a completed claim into a primary-key point lookup on the single monthly child partition instead of a scan of every monthly partition. The cost of a replay no longer grows with partition count (retention age). No behavior change on the authoritative path — the returned payload and the digest/partition-mismatch failure arms are unchanged. One incidental property changes: the read no longer detects a stray payload row in a different partition from the claim's authoritative one (this is necessarily dropped by partition pruning); cross-partition cardinality remains enforced at write time (update_complete) and re-asserted by the cleanup delete guard. ADR-0001 gains a read-path performance subsection. The production predicate is pinned by a registered mutation test; an EXPLAIN-based mechanism proof documents the pruning directly.
v0.4.0 — 2026-08-09
Hardening, ops-readiness, and enhancement release. No breaking contract change for existing
consumers — the minor bump carries new public observability surfaces (the telemetry default
attach helper, the :worker_timeout result class, the ETS cache reference adapter) and the
span-events-out-of-scope decision. Existing consumers are unchanged; all new surfaces are
opt-in.
- External-recovery adversarial-absence proof (H10): a test proving the re-execution
invariant against a lying-
:absentadapter, plus a normative section indocumentation/external-effects.mdstating the adapter MUST prove absence and the peer MUST enforce idempotency by operation key. No runtime guard — the trust is inherent to the design (ADR-0001). The library's:absenttrust is safe against a correct peer. - Worker timeout distinguished from disconnect (H11): the committed-claim worker's 30s
timeout now surfaces as a distinct
:worker_timeoutresult_class on[:ash_onetime, :store_uncertainty], separate from:disconnectedand:unknown. All three fail closed; the distinction is operational triage (pool/lock contention vs network partition). - Oban worker backoff + discard alert (H20, ADR-0005): the three maintenance workers
(Cleanup, Partition, Reap) declare a bounded, jittered
backoff/1(30–120 s) instead of the default exponential, so transient failures retry within the retention window. A documented discard-alert SQL names the operational signal for a stranded partition roll. - Telemetry default attach handler (H21):
AshOnetime.Telemetry.attach/0— an opt-in helper routing the closed event surface into a downstream:metricstream for a consumer's own aggregator. Notelemetry_metricsdependency; the value-free invariant is preserved. - Telemetry span structure (H22): documented that the library emits point events only
(never span events), with the reason (
:telemetry.span/3cannot preserve the value-free invariant — it force-injectstelemetry_span_contextand fires:exceptioninside the span before any caller rescue) and a recommended consumer-applied:telemetry.span/3wrapper. - Operations runbook (H23): three named procedures (backlog-stuck, partition-discard-detected, pool-saturated) with exact SQL/telemetry queries.
- ETS cache reference adapter (H30):
AshOnetime.Cache.Ets— bounded, TTL-aware, supervised, no third-party dependency. Makes the cache-degradation path reachable without a Redis dependency. - Admission unit tests (H31), key_source/claim property tests (H32): direct test coverage for the pure decision functions and the security-boundary invariants (the suite grows from 500 to 553 tests).
- Runtime security-surface docs (H33):
@docontoken.ex,key_source.ex,fingerprint.ex,telemetry.expublic functions. - CI-matrix-asserted compatibility documented (H34): CONTRIBUTING names the CI matrix as the guard against transitive semantic drift (not the dep bounds).
v0.3.0 — 2026-08-09
Security release: raises the Ash floor to the CVE-patched 3.31.1 and makes the architecture census robust to dependency framework-injection drift. Breaking dependency change — consumers on Ash 3.29.3–3.31.0 must bump Ash to ≥ 3.31.1 (the library is pre-1.0, so the minor version carries the break per the documented convention).
- Ash floor raised to 3.31.1 (from 3.29.3). EEF-CVE-2026-70395 (predicate injection in
manage_relationshipbelongs_to lookup disclosing secret lookup keys) and EEF-CVE-2026-69659 (memory exhaustion via unbounded keyset-cursor deserialization) affect Ash below 3.31.1; both are patched in 3.31.1.mix hex.audit— a required gate — now fails on the 3.29.3 lock. Consumers pinned to Ash 3.29.3–3.31.0 must bump Ash to ≥ 3.31.1. See ADR-0004 (Security-driven Ash floor). The CI matrix narrows from[3.29.3, 3.30.1, latest]to[3.31.1, latest]. - Export census version-tolerance (test harness): the documented public-export census no
longer breaks on Splode/Ash framework-injection drift. It derives each module's
project-authored exports from its own source AST, so framework-injected functions (e.g.
Splode 0.3.2's
keyword_list_options?/0) are auto-classified rather than hand-maintained, and the census guards exactly the project-owned public surface. This fixed a CI failure present since v0.1.0 across all Ash-matrix cells. Also fixes a seed-dependent flake in the ARCH-1 Splode-membership assertion (force-loads the module before the check).
v0.2.0 — 2026-08-09
Feature release: the DPoP replay fence, forward response-partition maintenance (SEC-5/6), and notebook-per-concern docs. New user-facing capabilities (minor bump); no contract break since v0.1.1 — existing consumers are unchanged, all new surfaces are opt-in.
- DPoP replay fence (admission): add
commit: :independent(default:with_action) on:one_time_nonceprotections. When set, the nonce claim commits in its own transaction before the action body runs (via the existingclaim_committedworker), so a body failure cannot make the proof reusable — RFC 9449 §11.1 request-attempt scope. A reused proof within the acceptance window is rejected with:nonce_already_usedvia the existing collision path. Opt-in: existing nonce consumers are byte-for-byte unchanged. Rejected (compile error) on:idempotency. No new table, migration, error code, or telemetry event. See ADR-0003 (Independent-commit nonce). Operational note: theclaim_committedworker uses a +1 connection checkout per in-flight protected request and a 30s timeout (fails closed); size the pool accordingly. - SEC-5 (data-layer): add forward monthly
response_payloadspartition creation (Store.roll_partitions/2,mix ash_onetime.roll_partitions,AshOnetime.Oban.PartitionWorker) plus a one-time forward migration (mix ash_onetime.gen.roll_forward). The install migration generates a fixed 13-month window; once retention exceeds it, payloads routed to_defaultand were never dropped (the drop path excludes_default), silently defeating bounded retention (ADR-0001:65 "reuse after retention is a new execution"). The roll keeps the window ahead of retention; the forward migration back-fills elapsed partitions, adds the index, and drains past-retention payloads stranded in_defaultvia a claim-scoped delete. The roll is idempotent and concurrency-safe (advisory-locked, boundedlock_timeout). Existing installs run the forward migration once. - SEC-6 (data-layer): add an index on
ash_onetime_idempotency_claims(response_partition). Cleanup's partition-empty check was a full scan per partition per cycle. - Telemetry: add
:partitions_createdto the[:ash_onetime, :cleanup]event's result-class enum (closed-schema extension, pinned by a mutation fixture). See Telemetry.
v0.1.1 — 2026-08-08
Adoption polish: a runnable Livebook walkthrough, richer Igniter installer, and adoption docs. No contract change; safe minor bump from v0.1.0.
- Add runnable Livebook notebooks — one per
concern (idempotency, one-time nonces, external recovery) — covering fresh execution, replay,
fingerprint conflict, nonce spend/reuse, and the external-effect protocol end-to-end against a
real PostgreSQL. Each notebook's code is regression-pinned by
test/ash_onetime/livebook_walkthrough_test.exs. - Extend the Igniter installer with a repeatable
--resource MyApp.MyResourceflag that wiresAshOnetime.Resourceinto a resource and scaffolds a starteronetimeblock. Non-resource targets and missing modules are rejected loudly instead of silently no-op'ing. - Add adoption docs: Recipes, Telemetry, Upgrading, and FAQ. Add a README "When to use this vs. hand-rolled idempotency" section and a "Try it" livebook pointer.
- Reorder Getting started to lead with the consumer quickstart (install → protect → handle the result); move the test-DB harness to CONTRIBUTING. Fix the stale "not published yet" line (the package is live on Hex).
v0.1.0 — 2026-08-08
- Breaking (DSL): collapse the dual
limitssurface into a singleprotect-level vocabulary. Response-size limits can no longer be declared on theresponseentity (response ..., limits: [...]); declare them onprotectinstead. Theprotect limits:option now accepts the full 11-key union vocabulary:max_key_bytes,max_token_bytes,max_scope_components,max_fingerprint_bytes,verifier_timeout_ms,max_cache_entry_bytes(key/verification/cache paths), andmax_response_bytes,max_response_depth,max_response_nodes,max_response_entries,max_response_scalar_bytes(response payload). All keys are validated at compile time. To migrate, move anyresponse ..., limits: [max_response_*: ...]keys ontoprotect ..., limits: [...]. - Make
AshOnetime.Errora Splode error of class:invalidso Ash recognizes it and preserves the typed:codethrough the action pipeline. Before, a protected-action failure was wrapped asAsh.Error.Unknown.UnknownErrorand the code (e.g.:nonce_already_used,:key_reused_with_different_request) was lost before it could reach the caller. AddAshOnetime.Error.code/1to recover the code from a leaf or class wrapper. Seedocumentation/errors.mdfor the code→HTTP table. - Add a caller-visible replayed-vs-fresh signal. After
Ash.create/2/Ash.run_action/2returns,AshOnetime.replayed?/1reports whether the result was a stored replay (true), a fresh execution (false), or carries no signal (nil— untracked execution, primitive-return action, or unprotected). The signal rides__metadata__[:ash_onetime]for tracked admission classes;:untrackedis deliberately not stamped to preserve untracked transparency. Seedocumentation/replay.md. - Broaden the Ash dependency requirement from
~> 3.29.0(only 3.29.x) to>= 3.29.3 and < 4.0.0, so the package installs across the whole Ash 3.x line. The floor is 3.29.3, not 3.29.0: EEF-CVE-2026-55736 (private action arguments settable by user input) affects Ash 3.29.0–3.29.2 and is fixed in 3.29.3. - Add a CI compatibility matrix (
.github/workflows/ci.yml), configured to run the full gate battery — includingmix hex.audit— against the 3.29.3 floor, each intermediate minor, and a floatinglatestAsh 3.x cell on every push and pull request once the repository is pushed to a GitHub remote. - Establish the standalone Mix package, PostgreSQL 18 test harness, package boundary checks, accepted architecture decision, and project documentation.
- Add the per-action Spark resource DSL, normalized introspection, precompile rejection boundary, fail-closed runtime stubs, compile-fixture battery, and mutation proofs.
- Add PostgreSQL-authoritative idempotency and one-time nonce admission with exact operation, scope, key, fingerprint, transaction, and failure-direction invariants.
- Add transactional CRUD and generic-action execution, classified typed response persistence,
digest-bound replay, and replay-safe lifecycle enforcement. The response contract digest binds
the codec options, so stored bytes cannot be reinterpreted under changed options; replayed
results carry the same
selected/tenantmetadata as a first execution. - Enforce the configurable
max_scope_components,max_fingerprint_bytes, andmax_response_byteslimit overrides at their declared values, not only their package ceilings. - Add bounded canonical encoding, HMAC-SHA-256 and Ed25519 signing, trusted verification facts, self-identifying tokens, and inclusive nonce windows.
- Add committed external-effect recovery points, stable peer operation keys, conservative ambiguous-outcome handling, and crash recovery.
- Add unpartitioned and operation-hash-partitioned claim layouts, date-partitioned response payloads, strict bounded cleanup, deletion guards, the prune task, and optional Oban cleanup.
- Add PostgreSQL-gated cache degradation, optional Plug header extraction, closed value-free telemetry, deterministic Igniter installation, and migration generation.
- Add system, architecture, mutation, documentation, exact Hex archive, unpacked consumer, and dependency audit release gates, run on the pinned Elixir 1.20.2 / Erlang/OTP 29 runtime.