clientFromDiscovery function
Creates a gRPC KumihoClient using Control Plane discovery.
- Uses discovery to find the correct data-plane endpoint.
- Sets
tenantIdso the base client injectsx-tenant-id. - If
tokenis omitted, the client falls back to the standard token loader.
As a cloud-safe fallback, when no token can be resolved and no explicit
controlPlaneUrl is supplied, a loopback self-hosted CE server is probed
first. If one is present a tokenless CE client is returned; otherwise the
normal cloud discovery path runs (and surfaces its usual "token required"
error). The CE probe never runs when a token or explicit endpoint is
present, preserving the cloud path unchanged.
Implementation
Future<KumihoClient> clientFromDiscovery({
String? token,
String? tenantHint,
String? controlPlaneUrl,
bool forceRefresh = false,
}) async {
final hasExplicitEndpoint =
controlPlaneUrl != null && controlPlaneUrl.trim().isNotEmpty;
final resolvedToken =
(token != null && token.trim().isNotEmpty) ? token : loadBearerToken();
// A user authenticated only via KUMIHO_FIREBASE_ID_TOKEN is a valid cloud
// credential that discoverTenant honours, but loadBearerToken() does not read
// that env var. Treat it (and the cached Firebase id token) as "has a token"
// too, otherwise such a user would be silently routed to a loopback CE server.
final firebaseToken = loadFirebaseToken();
final hasCloudToken =
(resolvedToken != null && resolvedToken.trim().isNotEmpty) ||
(firebaseToken != null && firebaseToken.trim().isNotEmpty);
if (!hasExplicitEndpoint && !hasCloudToken) {
final ceClient = await clientFromLocalCe();
if (ceClient != null) {
return ceClient;
}
}
final record = await discoverTenant(
controlPlaneUrl: controlPlaneUrl,
tenantHint: tenantHint,
forceRefresh: forceRefresh,
);
final uri = record.serverUrl;
final secure = uri.scheme.toLowerCase() == 'https';
final port = uri.hasPort ? uri.port : (secure ? 443 : 80);
return KumihoClient(
host: uri.host,
port: port,
secure: secure,
token: token,
tenantId: record.tenantId,
);
}