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.4.0. Set your dependency to the minor range to pick up patches automatically and review this page on each minor bump:

{:ash_onetime, "~> 0.4"}

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-:absent adapter, and a normative section in documentation/external-effects.md stating 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_timeout result_class on [:ash_onetime, :store_uncertainty], separate from :disconnected and :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 :metric stream for a consumer's own aggregator. No telemetry_metrics dependency.
  • Telemetry span structure (H22): documented that the library emits point events only (never span events), with the reason (span/3 cannot preserve the value-free invariant) and a recommended consumer-applied :telemetry.span/3 wrapper.
  • 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): @doc on token.ex, key_source.ex, fingerprint.ex, telemetry.ex public 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
end

The 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).