discoverTenant function
Resolves discovery info via Control Plane.
If forceRefresh is false, a non-expired cached result is used.
Implementation
Future<DiscoveryRecord> discoverTenant({
String? controlPlaneUrl,
String? firebaseToken,
String? tenantHint,
bool forceRefresh = false,
Duration timeout = const Duration(seconds: 10),
File? cacheFile,
}) async {
final cpUrl = (controlPlaneUrl == null || controlPlaneUrl.trim().isEmpty)
? getControlPlaneUrl()
: controlPlaneUrl.trim();
KumihoCredentials? credentials;
String? token;
if (firebaseToken != null && firebaseToken.trim().isNotEmpty) {
token = firebaseToken.trim();
} else {
// Prefer explicit env var for discovery.
final envToken = Platform.environment[AuthEnvVars.firebaseToken];
if (envToken != null && envToken.trim().isNotEmpty) {
token = envToken.trim();
} else {
// Fall back to cached credentials, optionally auto-refreshing.
credentials = loadCredentials();
final refreshed = await autoRefreshCredentials(credentials);
credentials = refreshed ?? credentials;
token = credentials?.idToken;
}
}
if (token == null || token.trim().isEmpty) {
throw ArgumentError(
'A Firebase ID token is required for discovery. '
'Set KUMIHO_FIREBASE_ID_TOKEN or login via kumiho-cli '
'(~/.kumiho/kumiho_authentication.json).',
);
}
final file = cacheFile ?? getDefaultDiscoveryCacheFile();
final key = (tenantHint == null || tenantHint.trim().isEmpty)
? _defaultCacheKey
: tenantHint.trim();
if (!forceRefresh) {
final cached = _loadFromCache(file, key);
if (cached != null) return cached;
}
final url = _buildDiscoveryUrl(cpUrl);
final body = <String, Object>{
if (tenantHint != null && tenantHint.trim().isNotEmpty)
'tenant_hint': tenantHint.trim(),
};
Future<http.Response> postWithToken(String bearer) {
return http
.post(
url,
headers: {
'Authorization': 'Bearer $bearer',
'Content-Type': 'application/json',
},
body: jsonEncode(body),
)
.timeout(timeout);
}
var response = await postWithToken(token);
// If the cached token is expired/invalid, retry once after forcing refresh.
if (response.statusCode == 401 &&
firebaseToken == null &&
(Platform.environment[AuthEnvVars.firebaseToken]?.trim().isEmpty ?? true) &&
credentials != null &&
credentials.refreshToken.isNotEmpty) {
final bodyText = response.body;
if (bodyText.contains('invalid_id_token')) {
final refreshed = await autoRefreshCredentials(credentials, forceRefresh: true);
if (refreshed != null && refreshed.idToken.isNotEmpty && refreshed.idToken != token) {
token = refreshed.idToken;
response = await postWithToken(token);
}
}
}
if (response.statusCode >= 400) {
final snippet = response.body.length > 200
? response.body.substring(0, 200)
: response.body;
throw HttpException(
'Discovery endpoint returned ${response.statusCode}: $snippet',
uri: url,
);
}
final decoded = jsonDecode(response.body);
final record = _parseDiscoveryRecord(decoded);
// Cache the raw JSON payload so we can re-parse it later.
if (decoded is Map<String, dynamic>) {
_storeToCache(file, key, decoded);
}
return record;
}