refactor(worker): OnlineTokenStore zu DpapiTokenStore verallgemeinert
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ClaudeDo.Data;
|
||||
|
||||
namespace ClaudeDo.Worker;
|
||||
|
||||
/// <summary>
|
||||
/// Persists a single secret encrypted with DPAPI (CurrentUser scope), one instance per file.
|
||||
/// Windows-only. Used for the Online-Inbox refresh token (~/.claudeDo/online-inbox.token) and
|
||||
/// the ticket-system PAT (~/.claudeDo/ticket.pat).
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class DpapiTokenStore
|
||||
{
|
||||
private readonly string _tokenPath;
|
||||
|
||||
public DpapiTokenStore(string tokenPath) => _tokenPath = tokenPath;
|
||||
|
||||
public static DpapiTokenStore InAppData(string fileName)
|
||||
=> new(Path.Combine(Paths.AppDataRoot(), fileName));
|
||||
|
||||
public void Save(string secret)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(secret);
|
||||
var cipher = ProtectedData.Protect(Encoding.UTF8.GetBytes(secret), null, DataProtectionScope.CurrentUser);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_tokenPath)!);
|
||||
File.WriteAllBytes(_tokenPath, cipher);
|
||||
}
|
||||
|
||||
public string? Read()
|
||||
{
|
||||
if (!File.Exists(_tokenPath)) return null;
|
||||
try
|
||||
{
|
||||
var plain = ProtectedData.Unprotect(File.ReadAllBytes(_tokenPath), null, DataProtectionScope.CurrentUser);
|
||||
return Encoding.UTF8.GetString(plain);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Exists() => File.Exists(_tokenPath);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
if (File.Exists(_tokenPath)) File.Delete(_tokenPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// DI-Marker: der Online-Inbox-Refresh-Token.
|
||||
public sealed record OnlineRefreshTokenStore(DpapiTokenStore Store);
|
||||
|
||||
/// DI-Marker: der Personal Access Token des Ticketsystems.
|
||||
public sealed record TicketPatStore(DpapiTokenStore Store);
|
||||
@@ -69,7 +69,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
private readonly RefineRunner _refineRunner;
|
||||
private readonly WorkerConfig _cfg;
|
||||
private readonly OnlineInboxConfig _onlineInboxConfig;
|
||||
private readonly OnlineTokenStore _onlineTokenStore;
|
||||
private readonly DpapiTokenStore _onlineTokenStore;
|
||||
private readonly Runner.PendingQuestionRegistry _pendingQuestions;
|
||||
private readonly LogRingBuffer? _logBuffer;
|
||||
private readonly ISessionSkillRegistry _skillRegistry;
|
||||
@@ -104,7 +104,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
RefineRunner refineRunner,
|
||||
WorkerConfig cfg,
|
||||
OnlineInboxConfig onlineInboxConfig,
|
||||
OnlineTokenStore onlineTokenStore,
|
||||
OnlineRefreshTokenStore onlineTokenStore,
|
||||
Runner.PendingQuestionRegistry pendingQuestions,
|
||||
ISessionSkillRegistry skillRegistry,
|
||||
LogRingBuffer? logBuffer = null,
|
||||
@@ -138,7 +138,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
_refineRunner = refineRunner;
|
||||
_cfg = cfg;
|
||||
_onlineInboxConfig = onlineInboxConfig;
|
||||
_onlineTokenStore = onlineTokenStore;
|
||||
_onlineTokenStore = onlineTokenStore.Store;
|
||||
_pendingQuestions = pendingQuestions;
|
||||
_skillRegistry = skillRegistry;
|
||||
_logBuffer = logBuffer;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ClaudeDo.Data;
|
||||
|
||||
namespace ClaudeDo.Worker.Online;
|
||||
|
||||
/// <summary>
|
||||
/// Persists the Zitadel refresh token encrypted with DPAPI (CurrentUser scope).
|
||||
/// Windows-only; the file lives at ~/.claudeDo/online-inbox.token.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class OnlineTokenStore
|
||||
{
|
||||
private readonly string _tokenPath;
|
||||
|
||||
public OnlineTokenStore()
|
||||
: this(Path.Combine(Paths.AppDataRoot(), "online-inbox.token")) { }
|
||||
|
||||
internal OnlineTokenStore(string tokenPath)
|
||||
{
|
||||
_tokenPath = tokenPath;
|
||||
}
|
||||
|
||||
public void Save(string refreshToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(refreshToken);
|
||||
var plain = Encoding.UTF8.GetBytes(refreshToken);
|
||||
var cipher = ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_tokenPath)!);
|
||||
File.WriteAllBytes(_tokenPath, cipher);
|
||||
}
|
||||
|
||||
public string? Read()
|
||||
{
|
||||
if (!File.Exists(_tokenPath)) return null;
|
||||
try
|
||||
{
|
||||
var cipher = File.ReadAllBytes(_tokenPath);
|
||||
var plain = ProtectedData.Unprotect(cipher, null, DataProtectionScope.CurrentUser);
|
||||
return Encoding.UTF8.GetString(plain);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
if (File.Exists(_tokenPath))
|
||||
File.Delete(_tokenPath);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace ClaudeDo.Worker.Online;
|
||||
public sealed class ZitadelAuthProvider : IOnlineAuthProvider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly OnlineTokenStore _tokenStore;
|
||||
private readonly DpapiTokenStore _tokenStore;
|
||||
private readonly OnlineInboxConfig _config;
|
||||
private readonly ILogger<ZitadelAuthProvider> _logger;
|
||||
|
||||
@@ -29,12 +29,12 @@ public sealed class ZitadelAuthProvider : IOnlineAuthProvider
|
||||
|
||||
public ZitadelAuthProvider(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
OnlineTokenStore tokenStore,
|
||||
OnlineRefreshTokenStore tokenStore,
|
||||
OnlineInboxConfig config,
|
||||
ILogger<ZitadelAuthProvider> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_tokenStore = tokenStore;
|
||||
_tokenStore = tokenStore.Store;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Threading;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker;
|
||||
using ClaudeDo.Worker.Agents;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.External;
|
||||
@@ -206,12 +207,13 @@ builder.Services.AddMcpServer()
|
||||
.WithTools<TaskRunMcpService>()
|
||||
.WithTools<TaskRunFindingsMcpTools>();
|
||||
|
||||
// OnlineInboxConfig and OnlineTokenStore are always registered so hub methods work
|
||||
// OnlineInboxConfig and the refresh-token store are always registered so hub methods work
|
||||
// even when sync is disabled. The sync stack (api client, auth, hosted service) is
|
||||
// only registered when enabled.
|
||||
builder.Services.AddSingleton(cfg.OnlineInbox);
|
||||
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
|
||||
builder.Services.AddSingleton<OnlineTokenStore>();
|
||||
builder.Services.AddSingleton(new OnlineRefreshTokenStore(DpapiTokenStore.InAppData("online-inbox.token")));
|
||||
builder.Services.AddSingleton(new TicketPatStore(DpapiTokenStore.InAppData("ticket.pat")));
|
||||
#pragma warning restore CA1416
|
||||
|
||||
if (cfg.OnlineInbox.Enabled)
|
||||
|
||||
Reference in New Issue
Block a user