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.
The SDK exposes a sealed SynheartAuthError hierarchy in Dart (mirrored as enum-style errors in Kotlin and Swift). Every error has a stable code that maps 1:1 to RFC-AUTH-MOBILE-0001 §11.
Taxonomy
| Code | Dart class | Trigger | SDK behaviour |
|---|
NETWORK_ERROR | NetworkError(message) | Challenge or register HTTP call failed | Retry with backoff (RFC §12.1). |
CHALLENGE_EXPIRED | ChallengeExpired | Server rejects stale challenge | Re-fetch a new challenge. |
ATTESTATION_UNAVAILABLE | AttestationUnavailable | Device/OS doesn’t support attestation | Surface to developer; cannot proceed. |
ATTESTATION_FAILED | (subset of ServerError) | Attestation provider rejects | Retry once; then surface. |
KEY_INVALIDATED | KeyInvalidated | Hardware key no longer usable | Transition to keyInvalid; next registerDevice wipes and restarts. |
KEYSTORE_ERROR | StorageError | Android Keystore op failed | Surface with platform details. |
SECURE_ENCLAVE_ERROR | StorageError | iOS Secure Enclave op failed | Surface with platform details. |
SIGNING_FAILED | CryptoError | Signature computation failed | Return immediately; non-recoverable in-request. |
DEVICE_REVOKED | ServerError | Server marks device inactive | Force re-registration via resetDeviceIdentity. |
CLOCK_SKEW | ClockSkew | Timestamp freshness rejected | Apply offset correction; retry. |
ROTATION_FAILED | ServerError | Key rotation request rejected | Keep old key active; retry later. |
NONCE_REPLAY | ServerError | Server rejects duplicate nonce | Generate new nonce and retry once. |
ALREADY_REGISTERED | AlreadyRegistered | registerDevice called when state is registered | No-op fast-path; treat as success. |
NOT_REGISTERED | NotRegistered | signRequest called before registration | Call registerDevice first. |
NOT_CONFIGURED | NotConfigured | API used before configure(baseUrl) | Call configure once at app launch. |
REGISTRATION_IN_PROGRESS | RegistrationInProgress | Concurrent registerDevice | Wait for the in-flight call. |
CRYPTO_ERROR | CryptoError(message) | Generic crypto failure | Inspect message; usually surfaced to developer. |
STORAGE_ERROR | StorageError(message) | Keychain / EncryptedSharedPreferences failure | Inspect message; surface. |
INVALID_STATE_TRANSITION | InvalidStateTransition(from, to) | Programmer error; state machine misuse | Inspect call site. |
| (any other server code) | ServerError(code, serverMessage) | Catch-all for server-defined codes | Inspect; default surface to developer. |
SynheartAuthError.fromCode(code, msg?) maps platform-channel error codes back to the typed exception in Dart.
Key invalidation detection (RFC §11.1)
Hardware keys can become unusable due to:
- OS updates that rotate Secure Enclave / Keystore root keys.
- Biometric enrollment changes.
- Device security state changes (PIN reset, factory reset).
- Backup/restore between devices.
| Platform | Detection |
|---|
| Android | KeyPermanentlyInvalidatedException on Cipher.init/Signature.initSign. UnrecoverableKeyException when the alias no longer resolves. |
| iOS | errSecItemNotFound from SecKeyCopyExternalRepresentation or SecKeyCreateSignature. errSecAuthFailed when the biometric context can’t authenticate. LAError.biometryLockout for biometry-bound keys after too many failures. |
On detection:
- The SDK emits
KEY_INVALIDATED (Dart: KeyInvalidated).
- State transitions
registered → keyInvalid.
- The next
registerDevice(appId) call wipes local state and runs the full registration flow.
The SDK never silently retries with the invalid key.
Clock skew correction (RFC §11.2)
Mobile clocks drift. The SDK can’t force NTP, so it learns the offset from the server.
Server contract
When rejecting a request with CLOCK_SKEW, the response includes:
{
"error": "CLOCK_SKEW",
"server_timestamp": 1709312345,
"message": "Request timestamp too far from server time"
}
SDK behaviour
Future<void> correctClockSkew(double serverTimestamp) {
final localTimestamp = DateTime.now().millisecondsSinceEpoch / 1000;
final offsetMs = ((serverTimestamp - localTimestamp) * 1000).round();
// persist offsetMs as clock_offset_ms
// on next sign: adjusted_ts = local_ts + clock_offset_ms / 1000
}
The SDK exposes correctClockSkew(serverTimestamp) for hosts to call when they receive a CLOCK_SKEW error from any Synheart endpoint. Subsequent signatures use the corrected timestamp automatically.
The persisted clock_offset_ms survives app restarts. Per RFC §11.2, the SDK SHOULD periodically re-validate the offset by comparing with X-Synheart-Server-Time from successful responses (server-supplied; not currently emitted by all services).
Retry policy (RFC §12)
Challenge + registration
delay = min(base * 2^attempt + random(0, jitter), max_delay)
base = 1 second
jitter = 500 ms
max = 30 seconds
attempts = 5
Always re-fetch a new challenge after any register failure (challenges are single-use).
Signing
Signing failures are non-recoverable in-request — return error immediately. If KEY_INVALIDATED is detected, transition to keyInvalid and trigger re-registration before the next request.
Key rotation
Rotation failures MUST NOT block normal request signing. Backoff: base 60s, max 1 hour, attempts 3 per cycle. After max attempts, log warning and retry on next app launch.
Logging policy
Per RFC §13:
| Allowed (prod) | Never (prod) |
|---|
app_id | Raw signatures |
device_id (truncated/hashed) | Raw request bodies |
| Error codes | Attestation proof payloads |
| Timing metrics | Public key material (debug builds only with opt-in) |
| Rotation events | Clock offset values |
| State transitions | |
InvalidStateTransition should be logged at error level — it indicates a programmer bug.
- State Machine —
KeyInvalidated and InvalidStateTransition semantics.
- Registration — failure paths during the handshake.
- Signing —
CLOCK_SKEW, NONCE_REPLAY recovery.