diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index ae2f0ba..aa3451f 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -43,6 +43,26 @@ class HubEndpoint { String toString() => '${secure ? "https" : "http"}://$host:$port'; } +/// Outcome of a [HubClient.probe] health check. Distinguishes +/// "the hub is down" from "the hub is up but rejected our +/// credentials" — the two need different operator guidance. +enum HubProbeResult { + /// Hub responded with SERVING. + serving, + + /// Hub responded, but not with SERVING (starting up or + /// shutting down). + notServing, + + /// Hub is reachable but rejected the call as UNAUTHENTICATED + /// or PERMISSION_DENIED — the endpoint is fine, the token + /// is missing, wrong, or lacks scope. + authRejected, + + /// Connection-level failure (refused, timeout, TLS, DNS). + unreachable, +} + /// Re-exports of generated protobuf types so callers don't have /// to import the `generated/` directory directly. typedef CapabilityEntry = pb.CapabilityEntry; @@ -150,6 +170,26 @@ class HubClient { } } + /// Like [healthy], but keeps enough of the failure to tell an + /// auth rejection apart from a dead endpoint, so UIs can say + /// "check your token" instead of a misleading "unreachable". + Future probe() async { + try { + final r = await _hub.health(Empty()); + return r.state == pb.HealthStatus_State.SERVING + ? HubProbeResult.serving + : HubProbeResult.notServing; + } on grpc.GrpcError catch (e) { + if (e.code == grpc.StatusCode.unauthenticated || + e.code == grpc.StatusCode.permissionDenied) { + return HubProbeResult.authRejected; + } + return HubProbeResult.unreachable; + } catch (_) { + return HubProbeResult.unreachable; + } + } + /// All capabilities provided by installed modules. Future> listCapabilities() async { final r = await _admin.listCapabilities(Empty());