# `AttestoPhoenix.Schema.DeviceCode`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/schema/device_code.ex#L1)

Ecto schema + record bridge for the RFC 8628 device-code store
(`AttestoPhoenix.Store.EctoDeviceCodeStore`).

Backs `Attesto.DeviceCodeStore`: a device code is a mutable row that moves
through `pending` → (`approved` | `denied`) → `consumed`. `from_record/1`
spreads the core's `Attesto.DeviceCodeStore.entry()` map across the row's
columns for the initial `pending` insert; `to_entry/1` folds a loaded row back
into that contract shape. The mutating transitions are done as guarded atomic
`UPDATE`s in the store, not through this changeset.

Only the device code's `Attesto.Secret.hash/1` is stored, never the plaintext;
`user_code` is stored normalized.

# `t`

```elixir
@type t() :: %AttestoPhoenix.Schema.DeviceCode{
  __meta__: term(),
  client_id: term(),
  device_code_hash: term(),
  dpop_jkt: term(),
  expires_at: term(),
  granted_claims: term(),
  granted_scope: term(),
  id: term(),
  inserted_at: term(),
  last_polled_at: term(),
  resource: term(),
  scope: term(),
  status: term(),
  subject: term(),
  user_code: term()
}
```

# `from_record`

```elixir
@spec from_record(
  Attesto.DeviceCodeStore.entry(),
  keyword()
) :: Ecto.Changeset.t()
```

Build the insert changeset for a new `pending` device code from the core
store record. Fail-closed: a missing required field is rejected, not defaulted.

# `to_entry`

```elixir
@spec to_entry(t()) :: Attesto.DeviceCodeStore.entry()
```

Fold a loaded row into the `Attesto.DeviceCodeStore.entry()` contract shape.

# `to_pending_view`

```elixir
@spec to_pending_view(t()) :: Attesto.DeviceCodeStore.pending_view()
```

The non-consuming `Attesto.DeviceCodeStore.pending_view()` for the verification
page.

---

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