> ## Documentation Index
> Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# State Machine

> Device authentication state machine per RFC-AUTH-MOBILE-0001 §7.1

The mobile auth SDK runs an explicit state machine per `(app_id, device)`. Transitions are validated; invalid transitions throw `InvalidStateTransition`. The state is persisted alongside `device_id` so the SDK can resume across app restarts.

## States

| Dart enum           | Wire string           | Meaning                                                 |
| ------------------- | --------------------- | ------------------------------------------------------- |
| `unregistered`      | `"unregistered"`      | No device identity exists for this `app_id`.            |
| `challengeReceived` | `"challengeReceived"` | Challenge fetched from server, awaiting key generation. |
| `keyReady`          | `"keyReady"`          | Hardware keypair generated, awaiting attestation.       |
| `registering`       | `"registering"`       | Attestation proof generated, register call in flight.   |
| `registered`        | `"registered"`        | Device identity established, ready to sign.             |
| `keyInvalid`        | `"keyInvalid"`        | OS-detected key invalidation; requires re-registration. |

## Allowed transitions

```text theme={null}
unregistered       → challengeReceived
challengeReceived  → keyReady
keyReady           → registering
registering        → registered
registering        → unregistered      (failure → reset → retry)
registered         → registering       (key rotation)
registered         → keyInvalid        (OS detected key gone)
keyInvalid         → unregistered      (wipe and start fresh)
```

Any other transition throws `InvalidStateTransition`.

```text theme={null}
                 ┌─────────────┐
                 │ unregistered│ ◄─────────────┐
                 └─────┬───────┘               │
                       │ fetch challenge       │ wipe
                       ▼                       │
            ┌──────────────────┐               │
            │ challengeReceived│               │
            └─────┬────────────┘               │
                  │ generate key               │
                  ▼                            │
            ┌──────────┐                       │
            │ keyReady │                       │
            └────┬─────┘                       │
                 │ attest + register           │
                 ▼                             │
       ┌──────────────────┐                    │
       │   registering    │ ────fail──────────►│
       └─────┬────────────┘                    │
             │ success                         │
             ▼                                 │
       ┌────────────┐  rotate ┌─────────────┐  │
       │ registered │────────►│ registering │──┘
       └─────┬──────┘         └─────────────┘
             │ OS invalidation
             ▼
       ┌────────────┐  wipe   ┌─────────────┐
       │ keyInvalid │────────►│unregistered │
       └────────────┘         └─────────────┘
```

## Mapping to public API

| API call                       | Triggered transition                    |
| ------------------------------ | --------------------------------------- |
| `configure(baseUrl)`           | (no state change)                       |
| `isRegistered(appId)`          | (no state change)                       |
| `registerDevice(appId)` step 1 | `unregistered → challengeReceived`      |
| key generation                 | `challengeReceived → keyReady`          |
| attestation + register call    | `keyReady → registering`                |
| register success               | `registering → registered`              |
| register failure               | `registering → unregistered`            |
| `rotateKey(appId)`             | `registered → registering → registered` |
| OS detected key invalidation   | `registered → keyInvalid`               |
| `resetDeviceIdentity(appId)`   | any state → `unregistered`              |

## Persistence

The current state is stored alongside `device_id`, key alias, `platform`, `registered_at`, `key_rotated_at`, and `clock_offset_ms`.

| Platform | Storage                                                               |
| -------- | --------------------------------------------------------------------- |
| iOS      | Keychain (state + metadata) + Secure Enclave (key reference).         |
| Android  | EncryptedSharedPreferences (state + metadata) + Keystore (key alias). |

Private key bytes are **never** persisted in app-managed storage — they live only in hardware.

## Key invalidation detection (RFC §11.1)

The SDK transitions `registered → keyInvalid` automatically when the OS reports the key is no longer usable.

| Platform | Detection                                                                                                                                                  |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Android  | `KeyPermanentlyInvalidatedException` on any Keystore operation; `UnrecoverableKeyException` when alias no longer resolves.                                 |
| iOS      | `errSecItemNotFound` (key deleted from Secure Enclave); `errSecAuthFailed` (biometric context changed); `LAError.biometryLockout` for biometry-bound keys. |

On detection:

1. SDK emits `KeyInvalidated` error.
2. State transitions to `keyInvalid`.
3. The next `registerDevice(appId)` call wipes local state (`keyInvalid → unregistered`) and starts fresh.

The SDK never silently retries with the invalid key.

## State after `resetDeviceIdentity`

`resetDeviceIdentity(appId)` is the one-step path from any state back to `unregistered`. RFC §9.3 restricts when it's allowed:

* Server explicitly instructs identity reset (e.g. `DEVICE_REVOKED`).
* User explicitly requests it ("sign out of all devices").
* Key invalidation is detected and rotation is impossible.

It MUST NOT be called as a convenience retry. After it returns:

* The hardware key is deleted (best-effort; on hardware failures the alias is abandoned).
* `device_id` and metadata are cleared.
* Next API call requires a full re-registration.

## Related

* [Registration](/synheart-auth/registration) — happy-path transitions.
* [Errors](/synheart-auth/errors) — `KeyInvalidated`, `InvalidStateTransition`, recovery paths.
* [Signing](/synheart-auth/signing) — only `registered` enables this.
