What's hand-rolled
BuildProvider and Owner each hold a personal access token as a plain [JsonInclude]-annotated BuildProviderToken (a SemanticString) field:
BuildProvider.Token —
|
[JsonInclude] |
|
protected BuildProviderToken Token { get; private set; } = new(); |
|
[JsonInclude] |
Owner.Token (per-owner override) —
|
/// <summary> |
|
/// Optional token for this specific owner. If set, overrides the provider-level token. |
|
/// Useful for accessing private repositories in different organizations. |
|
/// </summary> |
|
[JsonInclude] |
|
public BuildProviderToken Token { get; internal set; } = new(); |
Both BuildProvider and Owner hang off AppData.BuildProviders (
|
public ConcurrentDictionary<BuildProviderName, BuildProvider> BuildProviders { get; set; } = []; |
), which
ktsu.AppDataStorage serializes as a whole and writes to a JSON file under the user's AppData directory on every save (
BuildMonitor.QueueSaveAppData() is called right after the token is set — e.g.
|
ShouldShowAccountIdPopup = false; |
|
} |
|
else if (ShouldShowTokenPopup) |
|
{ |
|
PopupInputString.Open(Strings.SetToken, Strings.Token, string.Empty, (result) => |
). The net effect: GitHub/Azure DevOps PATs (typically scoped to
repo/
workflow) end up sitting in plaintext JSON on disk rather than in an OS-native secret store.
What ktsu.CredentialCache provides
ktsu.CredentialCache.CredentialCache is an in-memory cache backed by a pluggable ICredentialStore, whose default (CredentialStoreFactory.CreateDefault()) routes to Windows Credential Manager, macOS Keychain, or Linux Secret Service (libsecret):
Why it's worth it
This isn't a line-count win — it's a genuine gap between what the app currently does (plaintext-on-disk PAT storage) and what a purpose-built OS-backed secret store gets right (encryption at rest, OS-level access control, no secret sitting in a file a backup tool or another local process can read straight off disk).
Compatibility
- Subject (
BuildMonitor) targets: net10.0
ktsu.CredentialCache targets: net9.0;net10.0 — compatible
- Dependency direction:
ktsu.CredentialCache's Directory.Packages.props references only ktsu.RoundTripStringJsonConverter, ktsu.Semantics.Strings, Polyfill, and System.Text.Json — no dependency on BuildMonitor, so no cycle
Sketch
Before (per provider/owner):
[JsonInclude]
protected BuildProviderToken Token { get; private set; } = new();
After:
PersonaGUID persona = /* derived/stored per provider or owner */;
CredentialCache.Instance.AddOrReplace(persona, new CredentialWithToken { Token = tokenValue.As<CredentialToken>() });
// ...
if (CredentialCache.Instance.TryGet(persona, out Credential? cred) && cred is CredentialWithToken t)
{
// use t.Token
}
AppData would then store only the PersonaGUID (or nothing at all, if the persona ID is derived deterministically from BuildProviderName/OwnerName), not the token itself.
Caveats
- This changes the shape of
BuildProvider/Owner's persisted state: existing AppData.json files with a Token field would need a one-time migration (read the old field once, write it into CredentialCache, then stop persisting it directly) rather than a silent swap.
CredentialStoreFactory.CreateDefault()'s Linux backend depends on libsecret (Secret Service) being available; headless/container environments without a Secret Service provider would need a fallback story, which is only relevant if BuildMonitor is ever run outside a desktop session.
- This is a
[JsonInclude]/internal implementation detail, not part of the module's tests or any public contract, so no public API changes for consumers of this repo.
What's hand-rolled
BuildProviderandOwnereach hold a personal access token as a plain[JsonInclude]-annotatedBuildProviderToken(aSemanticString) field:BuildProvider.Token—BuildMonitor/BuildMonitor/BuildProvider.cs
Lines 49 to 51 in 0b84482
Owner.Token(per-owner override) —BuildMonitor/BuildMonitor/Owner.cs
Lines 21 to 26 in 0b84482
Both
BuildProviderandOwnerhang offAppData.BuildProviders(BuildMonitor/BuildMonitor/AppData.cs
Line 15 in 0b84482
ktsu.AppDataStorageserializes as a whole and writes to a JSON file under the user's AppData directory on every save (BuildMonitor.QueueSaveAppData()is called right after the token is set — e.g.BuildMonitor/BuildMonitor/BuildProvider.cs
Lines 212 to 216 in 0b84482
repo/workflow) end up sitting in plaintext JSON on disk rather than in an OS-native secret store.What ktsu.CredentialCache provides
ktsu.CredentialCache.CredentialCacheis an in-memory cache backed by a pluggableICredentialStore, whose default (CredentialStoreFactory.CreateDefault()) routes to Windows Credential Manager, macOS Keychain, or Linux Secret Service (libsecret):CredentialCache.Instance.AddOrReplace(PersonaGUID persona, Credential credential)— persists before caching in memory (https://github.com/ktsu-dev/CredentialCache/blob/6286ebe59ae30768647f01fa50e22b579831a37e/CredentialCache/CredentialCache.cs#L140-L149)CredentialCache.Instance.TryGet(PersonaGUID persona, out Credential? credential)— loads from the store on cache miss (https://github.com/ktsu-dev/CredentialCache/blob/6286ebe59ae30768647f01fa50e22b579831a37e/CredentialCache/CredentialCache.cs#L120-L136)CredentialWithToken.Token(CredentialToken, aSemanticString) — a ready-made typed token credential (https://github.com/ktsu-dev/CredentialCache/blob/6286ebe59ae30768647f01fa50e22b579831a37e/CredentialCache/CredentialWithToken.cs)Why it's worth it
This isn't a line-count win — it's a genuine gap between what the app currently does (plaintext-on-disk PAT storage) and what a purpose-built OS-backed secret store gets right (encryption at rest, OS-level access control, no secret sitting in a file a backup tool or another local process can read straight off disk).
Compatibility
BuildMonitor) targets:net10.0ktsu.CredentialCachetargets:net9.0;net10.0— compatiblektsu.CredentialCache'sDirectory.Packages.propsreferences onlyktsu.RoundTripStringJsonConverter,ktsu.Semantics.Strings,Polyfill, andSystem.Text.Json— no dependency on BuildMonitor, so no cycleSketch
Before (per provider/owner):
After:
AppDatawould then store only thePersonaGUID(or nothing at all, if the persona ID is derived deterministically fromBuildProviderName/OwnerName), not the token itself.Caveats
BuildProvider/Owner's persisted state: existingAppData.jsonfiles with aTokenfield would need a one-time migration (read the old field once, write it intoCredentialCache, then stop persisting it directly) rather than a silent swap.CredentialStoreFactory.CreateDefault()'s Linux backend depends on libsecret (Secret Service) being available; headless/container environments without a Secret Service provider would need a fallback story, which is only relevant if BuildMonitor is ever run outside a desktop session.[JsonInclude]/internal implementation detail, not part of the module's tests or any public contract, so no public API changes for consumers of this repo.