chain-client-sdk-dart/lib/src/hub_client.dart
flemming-it 1eaa36b1a0 feat: HubClient.verifyEventChain + listServices
Mirrors the two new HubAdmin RPCs in fai_platform 0.10.44.
Studio's Doctor page consumes these directly. Generated proto
bindings refreshed to include VerifyEventChainResponse,
ServiceList, DeclaredService.

Bumps fai_dart_sdk 0.2.0 -> 0.3.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-05-05 22:05:40 +02:00

173 lines
5.4 KiB
Dart

// HubClient — typed Dart wrapper around the F∆I gRPC surface.
//
// Holds a single grpc.ClientChannel keyed by [HubEndpoint]. Each
// public method maps onto exactly one RPC on Hub or HubAdmin, so
// callers don't have to know which service hosts which call.
import 'package:grpc/grpc.dart';
import 'generated/fai/v1/hub.pb.dart' as pb;
import 'generated/fai/v1/hub.pbgrpc.dart' as grpc_hub;
import 'generated/google/protobuf/empty.pb.dart' show Empty;
/// Connection coordinates for a hub endpoint.
class HubEndpoint {
/// Hostname or IP. Defaults to localhost for the canonical
/// `fai serve` setup.
final String host;
/// gRPC port. Defaults to 50051.
final int port;
/// When true, use TLS. Phase 1+ will add credential support.
final bool secure;
const HubEndpoint({
this.host = '127.0.0.1',
this.port = 50051,
this.secure = false,
});
@override
String toString() => '${secure ? "https" : "http"}://$host:$port';
}
/// Re-exports of generated protobuf types so callers don't have
/// to import the `generated/` directory directly.
typedef CapabilityEntry = pb.CapabilityEntry;
typedef CapabilityList = pb.CapabilityList;
typedef LoggedEvent = pb.LoggedEvent;
typedef PendingApprovalEntry = pb.PendingApprovalEntry;
typedef DeclaredService = pb.DeclaredService;
typedef VerifyEventChainResponse = pb.VerifyEventChainResponse;
/// Typed client for the F∆I Hub gRPC surface. Construct once,
/// reuse for the process lifetime, call [close] on shutdown.
class HubClient {
final HubEndpoint endpoint;
final ClientChannel _channel;
final grpc_hub.HubClient _hub;
final grpc_hub.HubAdminClient _admin;
HubClient({this.endpoint = const HubEndpoint()})
: _channel = ClientChannel(
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
),
),
_hub = grpc_hub.HubClient(
ClientChannel(
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
),
),
),
_admin = grpc_hub.HubAdminClient(
ClientChannel(
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
),
),
);
/// Liveness probe. Returns true when the hub responds with
/// SERVING. False on any failure (includes connection refused).
Future<bool> healthy() async {
try {
final r = await _hub.health(Empty());
return r.state == pb.HealthStatus_State.SERVING;
} catch (_) {
return false;
}
}
/// All capabilities provided by installed modules.
Future<List<CapabilityEntry>> listCapabilities() async {
final r = await _admin.listCapabilities(Empty());
return r.capabilities;
}
/// Recent events from the audit log, newest-first. Pass an
/// empty `types` list for all event types.
Future<List<LoggedEvent>> eventLog({
int limit = 100,
List<String> types = const [],
}) async {
final req = pb.EventLogRequest()
..limit = limit
..eventTypes.addAll(types);
final r = await _admin.eventLog(req);
return r.events;
}
/// Pending approvals filtered by status. Defaults to all.
Future<List<PendingApprovalEntry>> listApprovals({
List<String> statuses = const [],
int limit = 100,
}) async {
final req = pb.ListApprovalsRequest()
..limit = limit
..statuses.addAll(statuses);
final r = await _admin.listApprovals(req);
return r.entries;
}
/// Approve a pending approval. The flow that posted it resumes
/// on its next polling tick.
Future<void> approve({
required String approvalId,
required String reviewer,
}) async {
final req = pb.DecideApprovalRequest()
..approvalId = approvalId
..reviewer = reviewer
..decision = pb.DecideApprovalRequest_Decision.APPROVE;
await _admin.decideApproval(req);
}
/// Reject a pending approval. The flow fails with the supplied
/// reason on its next polling tick.
Future<void> reject({
required String approvalId,
required String reviewer,
required String reason,
}) async {
final req = pb.DecideApprovalRequest()
..approvalId = approvalId
..reviewer = reviewer
..reason = reason
..decision = pb.DecideApprovalRequest_Decision.REJECT;
await _admin.decideApproval(req);
}
/// Walk the event-log hash chain on the hub. Returns total /
/// verified counts and an `tamperedAt` id (empty when clean).
Future<VerifyEventChainResponse> verifyEventChain() {
return _admin.verifyEventChain(Empty());
}
/// Host services declared in the operator config. Each entry
/// is a name + endpoint + tags; reachability is up to the
/// caller (typically a follow-up HTTP probe).
Future<List<DeclaredService>> listServices() async {
final r = await _admin.listServices(Empty());
return r.services;
}
/// Closes the gRPC channel. Idempotent.
Future<void> close() async {
await _channel.shutdown();
}
}