# `DatadeftAuth`

Framework-independent authentication using a supervised, separate Rust process.

The transport plus the magic-link/session and PoW APIs are implemented,
backed by either the crate-shipped in-memory store (`mode: :fake`) or
DynamoDB/SES with Secrets Manager key loading (`mode: :aws`). See the README
for source installation, tested platforms and application deployment checks.
A healthy transport does not mean authentication is configured or available.

Add `{DatadeftAuth, name: MyAuth}` to your application's supervision tree.
The name doubles as the child id, so give each supervised instance a
distinct name; unregistered (nil-name) instances share the default id and
need hand-written child specs to coexist under one supervisor.
Native work never runs inside BEAM. No request is automatically replayed after
a timeout or worker failure.

HTTP routes belong to the consuming application. The reference layout uses
`/auth/magic-link/*`, `/auth/pow/*` and `POST /auth/logout`; `/dev/*` is local
tooling only. In particular, `GET /auth/magic-link/confirm` serves a page,
while `POST /auth/magic-link/consume` calls `confirm/4`. See the README for
the full mapping; this module neither mounts routes nor sets HTTP cookies.

# `config`

```elixir
@type config() :: %{
  :mode =&gt; :fake | :aws,
  :service =&gt; map(),
  :email =&gt; map(),
  :secrets =&gt; map(),
  optional(:aws) =&gt; map(),
  optional(:pow) =&gt; map()
}
```

Authentication configuration sent to the executable on every (re)connect.
Without one the transport is health-only and every auth call returns a
configuration error. The executable validates the content fail-closed:

- `mode: :fake` uses the crate-shipped in-memory store/outbox (tests and
  local development); `mode: :aws` uses DynamoDB, SES and Secrets Manager
  through the deployment-role credential chain and requires the `aws` key.
- `service` carries `terms_version`/`privacy_version` plus optional
  lifetimes, `enforce_country`, `account_display`, `default_locale` and
  `locales`.
- `email` carries `link_base_url` (the token is appended), `subject`,
  `text` and optional `html`; bodies must contain `{magic_link_url}` and
  the subject must not (Subject headers are routinely logged in transit,
  and the URL carries the bearer token). The reference landing URL ends in
  `/auth/magic-link/landing?token=`; redirect mode uses
  `/auth/magic-link/open?token=`. The destination stays application-configured.
- `secrets`: fake mode takes `active_document` (and optionally
  `previous_document`), the auth secret document JSON strings delivered
  once over the private pipe. AWS mode takes `active_ref`/`previous_ref`
  (`name_or_arn` with `stage` or `version_id`) so key material never
  enters BEAM; inline documents are allowed only together with a local
  `endpoint_url`.
- `aws` (AWS mode only) carries `region`, `table`, `from_email`, optional
  `reply_to`, and the test-only `endpoint_url`/`fake_outbox` pair for
  DynamoDB Local runs.
- `pow` (optional) enables the proof-of-work admission gate: `difficulty`
  (leading zero hex characters, 1..64; document your production floor) and
  `proof_budget_per_solve` (admissions one solve buys over the proof's
  lifetime, shared across nodes), plus optional `challenge_max_age_secs`
  (default 120) and `proof_ttl_secs` (default 10800). Without this key the
  pow operations return a configuration error.

# `max_pending`

```elixir
@type max_pending() :: 1..1024
```

Validated bound of the :max_pending option.

# `option`

```elixir
@type option() ::
  {:name, atom()}
  | {:executable, String.t() | nil}
  | {:config, config() | nil}
  | {:request_timeout, request_timeout()}
  | {:max_pending, max_pending()}
  | {:restart_delay, restart_delay()}
```

Transport options. A nil name, executable or config selects the default,
same as omitting the key. Executable paths and the config are trusted
deployment configuration.

# `options`

```elixir
@type options() :: [option()]
```

Optional transport settings; duplicate and unknown keys are rejected.

# `request_timeout`

```elixir
@type request_timeout() :: 1..60000
```

Validated bound of the :request_timeout option, in milliseconds.

# `restart_delay`

```elixir
@type restart_delay() :: 1..5000
```

Validated bound of the :restart_delay option, in milliseconds.

# `status_result`

```elixir
@type status_result() :: {:ok, :transport_ready} | {:error, DatadeftAuth.Error.t()}
```

Transport health only, not authentication or AWS readiness.

# `confirm`

```elixir
@spec confirm(GenServer.server(), String.t(), String.t(), keyword()) ::
  {:ok, DatadeftAuth.Session.t()} | {:error, DatadeftAuth.Error.t()}
```

Confirms a landing: consumes the link and creates the session atomically.
In the reference HTTP layout this is `POST /auth/magic-link/consume`, never
the GET-only `/auth/magic-link/confirm` page.
`country` is the trusted-edge signal, never client input. On `:rejected`
or `:bad_request` clear the confirm cookie.

On `:unavailable`, `:timeout`, `:protocol` or worker failure the outcome is
**indeterminate by contract**, not failed: the reply may have been lost
after the session committed and the one-time link was consumed. The
library never replays the commit and keeps no durable commit plan. Offer
the user a fresh sign-in link; one repeated confirm with the same inputs
is harmless but answers `:rejected` when the first attempt had committed.
A session committed behind a lost reply holds no issued cookie, idles out
on its own and is revocable server-side.

# `fake_last_magic_link`

```elixir
@spec fake_last_magic_link(GenServer.server(), String.t()) ::
  {:ok, DatadeftAuth.DevMagicLink.t()} | {:error, DatadeftAuth.Error.t()}
```

Fake-mode test helper: the newest outbox email's landing URL for an
address. Refused outside `mode: :fake`; never call it in production code.

# `flow_state`

```elixir
@spec flow_state(GenServer.server(), String.t()) ::
  {:ok, DatadeftAuth.FlowState.t()} | {:error, DatadeftAuth.Error.t()}
```

Reads the flow state bound to a confirm cookie (redirect-landing mode).
On `usable: false` clear the confirm cookie.

# `landing`

```elixir
@spec landing(GenServer.server(), String.t()) ::
  {:ok, DatadeftAuth.Landing.t()} | {:error, DatadeftAuth.Error.t()}
```

Scanner-safe landing for an emailed token: validates without consuming and
returns the flow secrets to store client-side. The answer is uniform for
every unusable link state; render the same page either way.

# `logout`

```elixir
@spec logout(GenServer.server(), String.t(), keyword()) ::
  {:ok, %{revoked: boolean()}} | {:error, DatadeftAuth.Error.t()}
```

Revokes the session server-side. Clear the session cookie only after
`{:ok, %{revoked: true}}` or `revoked: false` (already invalid); on
`:unavailable` keep it and retry. Protect the calling route against CSRF.

Pass the same `country` trusted-edge signal you pass to
`validate_session/3`: logout validates the session before revoking it, so
a missing or mismatched signal on a country-pinned session answers
`revoked: false` while the server-side session stays live.

# `pow_admit`

```elixir
@spec pow_admit(GenServer.server(), String.t()) ::
  {:ok, %{admitted: boolean()}} | {:error, DatadeftAuth.Error.t()}
```

Admits one gated request by its proof cookie, spending one unit of the
solve's shared lifetime budget. `admitted: false` (an unusable proof or an
exhausted budget) means answer "solve a new challenge"; a failing budget
store is `{:error, %Error{code: :unavailable}}` and must refuse the
request — never admit on failure.

# `pow_challenge`

```elixir
@spec pow_challenge(GenServer.server()) ::
  {:ok, DatadeftAuth.PowChallenge.t()} | {:error, DatadeftAuth.Error.t()}
```

Mints a proof-of-work challenge for the application's `POST /auth/pow/create` route.
Serialize the struct as JSON for the shipping browser client verbatim.
Requires the `pow` configuration key.

# `pow_verify`

```elixir
@spec pow_verify(GenServer.server(), map()) ::
  {:ok, DatadeftAuth.PowVerification.t()} | {:error, DatadeftAuth.Error.t()}
```

Verifies a posted solution (the client's `{chg, sol, non, tim, tag}` JSON
as a map). On `admitted: true` set the returned `proof_cookie` as the
`dd_pow` cookie (HttpOnly, Secure, the configured proof TTL). On
`admitted: false` answer with one generic "solve a new challenge" response
whatever check failed.

# `request_link`

```elixir
@spec request_link(GenServer.server(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, DatadeftAuth.Error.t()}
```

Requests a magic-link email. The public response to the requester must stay
the same generic acceptance whatever the returned reason label says; the
label (for example `"magic_link_request_sent"`) is for internal logs and
metrics only and never carries identifiers.

Options: `terms_accepted`, `privacy_accepted` (both default false; consent
is the application's statement, not a default) and `locale` (a hint
resolved against the configured allowlist).

# `start_link`

```elixir
@spec start_link(options()) :: GenServer.on_start()
```

Starts the transport owner. Readiness is asynchronous and checked with status/1.

# `status`

```elixir
@spec status(GenServer.server()) :: status_result()
```

Checks transport health; a refused initialization returns :configuration until the child is restarted with corrected config.

# `validate_session`

```elixir
@spec validate_session(GenServer.server(), String.t(), keyword()) ::
  {:ok, DatadeftAuth.Validation.t()} | {:error, DatadeftAuth.Error.t()}
```

Validates a session cookie (read-only) and re-issues it when due. Send a
returned `refreshed_cookie` with the normal session cookie settings on a
`no-store` response. Refresh is best-effort: an unavailable mint key or
entropy failure does not invalidate an already verified session; its existing
idle/absolute expiry still applies. `valid: false` means sign in again;
clear the cookie.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
