import 'dart:io'; import 'package:path/path.dart' as p; /// Operator-managed registry auth token, kept at /// `~/.fai/registry-token` (mode 0600 on Unix). The hub reads /// this file at install time when `FAI_REGISTRY_TOKEN` is unset /// — see `download_to_temp` in `crates/fai_hub/src/lib.rs`. /// /// Studio writes the file directly so a fresh install never /// requires the operator to fiddle with shell env vars. class RegistryToken { static String _faiHome() { final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; if (home == null || home.isEmpty) { throw StateError('Cannot resolve home directory (no HOME / USERPROFILE)'); } return p.join(home, '.fai'); } /// Absolute path to the token file. static String get path => p.join(_faiHome(), 'registry-token'); /// True iff the file exists with a non-empty trimmed body. static Future isConfigured() async { final f = File(path); if (!await f.exists()) return false; final content = await f.readAsString(); return content.trim().isNotEmpty; } /// Length of the trimmed token, or null when the file is /// missing / empty. Used for status display ("Configured /// (40 chars)") without ever surfacing the secret. static Future charCount() async { final f = File(path); if (!await f.exists()) return null; final content = await f.readAsString(); final trimmed = content.trim(); return trimmed.isEmpty ? null : trimmed.length; } /// Persist [token] to disk, creating `~/.fai/` if needed. /// On Unix the file is chmod-ed to 0600 (owner read/write /// only). On Windows the default user-ACL is left alone — /// best-effort, no PowerShell handshake. static Future write(String token) async { final f = File(path); await f.parent.create(recursive: true); await f.writeAsString(token.trim(), flush: true); if (Platform.isLinux || Platform.isMacOS) { try { await Process.run('chmod', ['600', f.path]); } catch (_) { // best-effort; if chmod is unavailable the file still // exists with default perms (usually 0644, world-readable // but only on the operator's own machine). } } } /// Delete the token file. No-op when the file doesn't exist. static Future delete() async { final f = File(path); if (await f.exists()) await f.delete(); } }