feat: probe() — health check that separates auth rejection from unreachable
Some checks failed
Security / Security check (push) Failing after 2s

healthy() collapses every failure to false, so a hub that is up
but rejects the bearer token was indistinguishable from a dead
endpoint. probe() returns serving / notServing / authRejected
(UNAUTHENTICATED or PERMISSION_DENIED) / unreachable so client
UIs can point the operator at the token instead of the wire.
healthy() is unchanged.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-15 04:45:16 +02:00
parent 9141a91314
commit ec3b684e6e

View file

@ -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<HubProbeResult> 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<List<CapabilityEntry>> listCapabilities() async {
final r = await _admin.listCapabilities(Empty());