Forge
csharp3407750f
1using System.Text.Json;
2using System.Text.Json.Serialization;
3
4namespace AgentForge.Credentials;
5
6public 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
127public 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
136public sealed record ForgeCredentialHostEntry(string HostKey, ForgeHostCredential Credential);
137
View only · write via MCP/CIDE