// 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 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> 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> eventLog({ int limit = 100, List 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> listApprovals({ List 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 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 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 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> listServices() async { final r = await _admin.listServices(Empty()); return r.services; } /// Closes the gRPC channel. Idempotent. Future close() async { await _channel.shutdown(); } }