Version-to-version migration notes. ash_onetime follows semantic versioning: breaking DSL
or contract changes bump the minor version (the library is pre-1.0), and each breaking
change lands here with the exact edit to make.
The published package is v0.5.0. Set your dependency to the minor range to pick up patches automatically and review this page on each minor bump:
{:ash_onetime, "~> 0.5"}v0.5.0 — security hardening from the independent code review
v0.5.0 lands the sixteen findings (M1–M5, L1–L11) from the v0.4.0 independent code review. It is a minor bump because four changes are consumer-visible and require action on upgrade; the rest are internal hardening (no consumer action). Read the four items below before upgrading.
CONSUMER-VISIBLE — PartitionWorker moved to a dedicated
:ash_onetime_partitionsOban queue (L4). Forward partition creation is the retention-safety path; it previously shared:ash_onetime_cleanupwith routine cleanup. Add the queue to your Oban config orPartitionWorkerjobs sit unscheduled and bounded retention silently degrades past the install window:config :my_app, Oban, queues: [ash_onetime_cleanup: 1, ash_onetime_reap: 1, ash_onetime_partitions: 1]The discard-alert SQL and the partition-discard triage in
operations.mdnow name the new queue.CONSUMER-VISIBLE — protected resources declaring a reserved-named attribute now fail to compile (M2). A protected resource that declares an attribute named
:key,:issued_at,:expires_at,:verification_state, or:algorithm— even with noaccepton any action — now fails compilation (it previously compiled and was caught only at runtime byreject_reserved/1). If a protected resource has such an attribute, rename it (e.g.:idempotency_keyinstead of:key); reserved names are trusted local facts the verification path derives itself and may not come from caller input.CONSUMER-VISIBLE — the
:clockverify-option override is off by default in every build (M5). The gate changed fromMix.env() == :test(which aMIX_ENV=test mix deps.compileconsumer could accidentally ship live) toApplication.compile_env(:ash_onetime, :allow_clock_override, false). The override is now disabled unless explicitly configured, regardless ofMIX_ENV. If your test suite pins verification time via the:clockoption, set it inconfig/test.exs:config :ash_onetime, allow_clock_override: true(set BEFORE compiling
ash_onetime— the gate is read at build time).CONSUMER-VISIBLE — Oban worker error tuples now carry the inner reason (L5).
{:error, :reap_failed}became{:error, {:reap_failed, reason}}(and likewise for:roll_partitions_failed,:cleanup_failed). Oban serializes the tuple intojob.errors; the distinguishable store cause (:lock_timeout/:disconnected/:store_invariant/ …) now survives exhaustion. If you pattern-match the old bare atom, update to the 2-tuple inner shape. Retry/discard semantics are unchanged.
The remaining twelve findings (M1, M3, M4, L1, L2, L3, L6, L7, L8, L9, L10, L11) are internal hardening with no consumer action — the bounded callback context, the consolidated constant-time comparator, the per-tenant partition-roll lock, the reap floor, the store telemetry event, the cache-key framing, the roll-forward namespace scoping, the change/generic_action dedup, the compile-cycle diagnostic, and the dropped dead spec arm. See the CHANGELOG for the full list.
v0.4.0 — hardening, ops-readiness, and enhancement
v0.4.0 is a hardening + ops-readiness + 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 (H22). Existing consumers are unchanged; all new
surfaces are opt-in.
- External-recovery adversarial-absence proof + normative doc (H10): a test proving the
re-execution invariant against a lying-
:absentadapter, and 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). - 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 that routes the closed event surface into a downstream:metricstream for a consumer's own aggregator. Notelemetry_metricsdependency. - Telemetry span structure (H22): documented that the library emits point events only
(never span events), with the reason (
span/3cannot preserve the value-free invariant) 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. - Admission unit tests (H31), key_source/claim property tests (H32): direct test coverage for the pure decision functions and the security-boundary invariants.
- 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).
Ash floor raised to 3.31.1 (v0.3.0, security-driven)
v0.3.0 tightens the Ash requirement from >= 3.29.3 and < 4.0.0 to
>= 3.31.1 and < 4.0.0. Two Ash advisories published during the v0.2.0 window affect Ash
below 3.31.1, both patched in 3.31.1: EEF-CVE-2026-70395 (predicate injection in
manage_relationship belongs_to lookup disclosing secret lookup keys) and
EEF-CVE-2026-69659 (memory exhaustion via unbounded keyset-cursor deserialization). A security
library must not admit a vulnerable floor. Bump Ash to ≥ 3.31.1, then bump ash_onetime:
{:ash_onetime, "~> 0.3"}See ADR-0004 (Security-driven Ash floor). The CI compatibility matrix narrows from
[3.29.3, 3.30.1, latest] to [3.31.1, latest].
Forward response-partition maintenance (apply once to existing installs)
Two data-layer fixes shipped for the response store: the response_partition index (cleanup's
partition-empty check was a full scan) and forward monthly partition creation (payloads past
the install window routed to _default and were never dropped, silently defeating bounded
retention). Greenfield installs on the current version get both automatically from the install
migration. Existing installs should generate and run the forward migration once:
mix ash_onetime.gen.roll_forward --repo MyApp.Repo --months 18
mix ecto.migrate
It adds the index, back-fills the elapsed+forward partitions, and drains past-retention
payloads stranded in _default. After that, schedule mix ash_onetime.roll_partitions (or
AshOnetime.Oban.PartitionWorker) on a cadence ahead of your retention horizon — see
Operations. This is additive (no DSL/contract
change) and does not require a dependency version bump.
DPoP replay fence (commit: :independent)
Additive: a new opt-in commit: option on :one_time_nonce protections (default
:with_action). No migration, no schema change, no new error code. Existing nonce consumers are
unchanged. Adopt by adding commit :independent to a nonce protect block whose proof should
survive action-body failure (RFC 9449 §11.1). See ADR-0003 (Independent-commit nonce) and the
operational characteristics for
pool-sizing notes. Declaring commit: on :idempotency is now a compile error (idempotency
commits with its effect).
Single limits surface (shipped in v0.1.0)
The dual limits surface collapsed to one protect-level vocabulary in v0.1.0. Response-size
limits are declared on the protect block, not the response entity:
protect :charge do
strategy :idempotency
# ...
response MyApp.ChargeCodec, fields: [:id, :status], classify: MyApp.ChargeClassifier
limits max_response_bytes: 8_388_608
endThe protect limits: option accepts the full 11-key union — the key/verification/cache
keys (max_key_bytes, max_token_bytes, max_scope_components, max_fingerprint_bytes,
verifier_timeout_ms, max_cache_entry_bytes) and the response-payload keys
(max_response_bytes, max_response_depth, max_response_nodes, max_response_entries,
max_response_scalar_bytes). All keys are validated at compile time. If you are upgrading
from a pre-v0.1.0 snapshot, move any response ..., limits: [max_response_*: ...] onto
protect ..., limits: [...].
This change is additive in coverage (no limit is lost) and removes a redundant configuration surface where two places could spell overlapping limits.
Between releases
Non-breaking additions (new optional integrations, new introspection helpers, new guides) ship in patch or minor releases and need no migration. Check the CHANGELOG for the full list per release.
If you depend on a private (non-documented) module or function, it may change in any release
— the public contract is the documented DSL, the modules in the API reference, and the
behaviours (AshOnetime.Codec, the classify/2 contract on AshOnetime.ResponseClassifier,
verification callbacks returning AshOnetime.Verified).