| 1 | using System.Text.Json; |
| 2 | using System.Text.Json.Serialization; |
| 3 | |
| 4 | namespace AgentForge.Credentials; |
| 5 | |
| 6 | public sealed class ForgeCredentialStore |
| 7 | { |
| 8 | private static readonly JsonSerializerOptions JsonOptions = new() |
| 9 | { |
| 10 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
| 11 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, |
| 12 | WriteIndented = true, |
| 13 | }; |
| 14 | |
| 15 | private readonly string _filePath; |
| 16 | private readonly Lock _lock = new(); |
| 17 | |
| 18 | public ForgeCredentialStore(string? filePath = null) => |
| 19 | _filePath = filePath ?? GetDefaultFilePath(); |
| 20 | |
| 21 | public static string GetDefaultFilePath() |
| 22 | { |
| 23 | var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); |
| 24 | return Path.Combine(home, ".forge", "credentials.json"); |
| 25 | } |
| 26 | |
| 27 | public ForgeHostCredential? TryGet(string baseUrl) |
| 28 | { |
| 29 | var key = ForgeHostUrl.NormalizeKey(baseUrl); |
| 30 | lock (_lock) |
| 31 | { |
| 32 | var file = LoadFile(); |
| 33 | return file.Hosts.TryGetValue(key, out var entry) ? entry : null; |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | public string? TryGetToken(string baseUrl) => TryGet(baseUrl)?.Token; |
| 38 | |
| 39 | public void Save(string baseUrl, string token, string? tokenName = null) |
| 40 | { |
| 41 | ArgumentException.ThrowIfNullOrWhiteSpace(token); |
| 42 | var key = ForgeHostUrl.NormalizeKey(baseUrl); |
| 43 | lock (_lock) |
| 44 | { |
| 45 | var file = LoadFile(); |
| 46 | file.Hosts[key] = new ForgeHostCredential |
| 47 | { |
| 48 | Token = token.Trim(), |
| 49 | TokenName = string.IsNullOrWhiteSpace(tokenName) ? null : tokenName.Trim(), |
| 50 | SavedAt = DateTimeOffset.UtcNow, |
| 51 | }; |
| 52 | WriteFile(file); |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | public bool Remove(string baseUrl) |
| 57 | { |
| 58 | var key = ForgeHostUrl.NormalizeKey(baseUrl); |
| 59 | lock (_lock) |
| 60 | { |
| 61 | var file = LoadFile(); |
| 62 | if (!file.Hosts.Remove(key)) |
| 63 | return false; |
| 64 | WriteFile(file); |
| 65 | return true; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | public IReadOnlyList<ForgeCredentialHostEntry> ListHosts() |
| 70 | { |
| 71 | lock (_lock) |
| 72 | { |
| 73 | var file = LoadFile(); |
| 74 | return file.Hosts |
| 75 | .Select(pair => new ForgeCredentialHostEntry(pair.Key, pair.Value)) |
| 76 | .OrderBy(entry => entry.HostKey, StringComparer.Ordinal) |
| 77 | .ToList(); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | private ForgeCredentialsFile LoadFile() |
| 82 | { |
| 83 | if (!File.Exists(_filePath)) |
| 84 | return new ForgeCredentialsFile(); |
| 85 | |
| 86 | try |
| 87 | { |
| 88 | var json = File.ReadAllText(_filePath); |
| 89 | return JsonSerializer.Deserialize<ForgeCredentialsFile>(json, JsonOptions) ?? new ForgeCredentialsFile(); |
| 90 | } |
| 91 | catch |
| 92 | { |
| 93 | return new ForgeCredentialsFile(); |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | private void WriteFile(ForgeCredentialsFile file) |
| 98 | { |
| 99 | var directory = Path.GetDirectoryName(_filePath); |
| 100 | if (!string.IsNullOrEmpty(directory)) |
| 101 | Directory.CreateDirectory(directory); |
| 102 | |
| 103 | var json = JsonSerializer.Serialize(file, JsonOptions); |
| 104 | File.WriteAllText(_filePath, json); |
| 105 | |
| 106 | if (!OperatingSystem.IsWindows()) |
| 107 | { |
| 108 | try |
| 109 | { |
| 110 | File.SetUnixFileMode(_filePath, UnixFileMode.UserRead | UnixFileMode.UserWrite); |
| 111 | } |
| 112 | catch |
| 113 | { |
| 114 | // Best effort on platforms without chmod. |
| 115 | } |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | private sealed class ForgeCredentialsFile |
| 120 | { |
| 121 | public int Version { get; set; } = 1; |
| 122 | |
| 123 | public Dictionary<string, ForgeHostCredential> Hosts { get; set; } = new(StringComparer.Ordinal); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | public sealed class ForgeHostCredential |
| 128 | { |
| 129 | public string Token { get; set; } = ""; |
| 130 | |
| 131 | public string? TokenName { get; set; } |
| 132 | |
| 133 | public DateTimeOffset SavedAt { get; set; } |
| 134 | } |
| 135 | |
| 136 | public sealed record ForgeCredentialHostEntry(string HostKey, ForgeHostCredential Credential); |
| 137 | |