logout method

Future<AuthLogoutResult> logout({
  1. required Dio apiDio,
  2. required Credentials creds,
})

Revokes the stored keyId; falls back to a prefix match against listKeys when the file predates that field. Revoke is not idempotent — a 400 (already revoked, unknown id, someone else's key) from the revoke call itself is read as "already gone", not an error.

logout authenticates AS the key it is revoking, so when that exact key is already dead (revoked from the phone, or the row is gone), authMiddleware 401s every call with it — including the listKeys fallback — before the request ever reaches the revoke logic that produces the 400 above, with the message API key revoked. That 401 means the same thing the 400 does: nothing left to revoke.

A different 401 — Invalid API key (the hash matches no row at all) or User not found — means this key does not resolve against the environment logout is running against at all, most likely an --env mismatch with the login that minted it. That is not "safe to delete": the key may still be live elsewhere, and the local file is its only record, so LogoutOutcome.unresolvedInEnv keeps it.

Implementation

Future<AuthLogoutResult> logout({
  required Dio apiDio,
  required Credentials creds,
}) async {
  try {
    final keyId =
        creds.keyId ?? await _findKeyIdByPrefix(apiDio, creds.apiKey);
    if (keyId == null) {
      return const AuthLogoutResult(LogoutOutcome.alreadyGone);
    }

    await apiDio.delete('${Endpoints.apiKeys}/$keyId');
    return const AuthLogoutResult(LogoutOutcome.revoked);
  } on DioException catch (e) {
    final status = e.response?.statusCode;
    final data = e.response?.data;
    final message = data is Map ? data['message'] as String? : null;

    if (status == 400 || (status == 401 && message == 'API key revoked')) {
      return const AuthLogoutResult(LogoutOutcome.alreadyGone);
    }
    if (status == 401) {
      return AuthLogoutResult(LogoutOutcome.unresolvedInEnv, detail: message);
    }
    rethrow;
  }
}