ash_onetime protects keyed effects against concurrent duplication and replay within an
explicit namespace. It does not authenticate a caller by itself, provide delivery, or turn
an unrecoverable external API into a transactional peer.
Trust boundaries
- PostgreSQL is the sole admission authority. A cache cannot admit or reject work.
- Scope is mandatory. Missing tenant, attribute, argument, or resolver data is an error.
- Operation identity is derived from the resource and action; callers cannot supply it.
- Verification callbacks and minters return trusted local
AshOnetime.Verifiedfacts. Action input remains untrusted even when field names resemble trusted state. - Stored responses are bound to locator hashes, request fingerprint, codec, SHA-256 digest, exact payload bytes, and declared Ash return shape before replay.
- One-time nonce store uncertainty fails closed. Only a proven never-dispatched idempotency checkout may use an explicitly enabled untracked path.
- External recovery treats every outcome except
{:ok, result}and authoritative:absentas ambiguous and never retries the peer effect under a new decision.
Canonical encoding is bounded, type-tagged, length-framed, and deterministic. Token verification binds exact canonical body bytes, expected namespace, algorithm, key identifier, issuance, and expiry. HMAC key material must declare same-service trust. Ed25519 separates private signing and public verification roles. Key resolution is purpose-specific; secrets must remain in the consumer's secret store and must not be logged or committed.
Named misuses
Misuse: using idempotency as replay defense. Idempotency returns a stored response on reuse;
it does not reject a captured signed request. Use :one_time_nonce.
Misuse: letting nonce admission inherit idempotency's optional untracked failure direction. Nonce store failure and uncertainty must reject; there is no nonce fail-open configuration.
Misuse: using a shared HMAC secret across a separated signer/verifier boundary. Anyone who can verify can then forge. Use Ed25519 or a verifier callback backed by the provider's supported scheme.
Misuse: declaring multitenancy strategy :attribute without the tenant discriminator in scope.
Attribute-multitenant resources share one physical set of claim tables across every tenant, so
cross-tenant isolation rests entirely on the tenant being part of the scope; omit it and one
tenant is served another's stored response or burns another's nonce. ash_onetime rejects such
a resource at compile time — the scope must include {:attribute, <tenant_attribute>} or a
{:tenant, module} resolver. (Context multitenancy routes each tenant to its own schema and
needs no scope entry for isolation.)
Retention and cleanup are security boundaries. Deleting a nonce while its acceptance window is
still open can admit a replay. The acceptance window is evaluated on the application clock while
cleanup eligibility is evaluated on the PostgreSQL clock, so a spent nonce is retained for a
configurable clock-skew safety margin (config :ash_onetime, :cleanup_clock_skew_margin_seconds,
default 1 second) beyond its acceptance horizon. Cleanup is therefore strictly later than the
acceptance window as long as the PostgreSQL clock is not ahead of the application clock by more
than that margin — keep both clocks synchronized (e.g. via NTP), and raise the margin for looser
synchronization. Processing external claims are retained for recovery until they are either
completed or reaped. An abandoned processing claim that never settles would otherwise be immortal
(cleanup skips it and the delete guard forbids deleting it), so a caller driving external-effect
actions with distinct keys and abandoning each after commit could accumulate unbounded undeletable
rows — a storage denial of service. The opt-in reaper (mix ash_onetime.reap) deletes such claims past a
separate, much longer abandonment horizon, through a sanctioned delete path: a processing claim is
removable only when it is older than both the operator's horizon and a hard 1-day floor and past
its own retention horizon, all re-enforced by the delete guard. Recoverability is thereby bounded
by max(retention, abandonment horizon) rather than unbounded, and a retry after reaping is a new
execution with a new peer operation key. The reaper bounds steady-state growth, not a burst
(residual ≈ admission rate × that window); operators tune it with edge rate limiting and reap
cadence.
Telemetry is deliberately value-free to avoid exporting keys, tokens, signatures, payloads,
or verifier identities.
Report suspected vulnerabilities through the private process in SECURITY.