diff --git a/src/ClaudeDo.Worker/DpapiTokenStore.cs b/src/ClaudeDo.Worker/DpapiTokenStore.cs
new file mode 100644
index 00000000..2d732316
--- /dev/null
+++ b/src/ClaudeDo.Worker/DpapiTokenStore.cs
@@ -0,0 +1,57 @@
+using System.Runtime.Versioning;
+using System.Security.Cryptography;
+using System.Text;
+using ClaudeDo.Data;
+
+namespace ClaudeDo.Worker;
+
+///
+/// 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).
+///
+[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);
diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
index 69818515..b30d5562 100644
--- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs
+++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
@@ -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;
diff --git a/src/ClaudeDo.Worker/Online/OnlineTokenStore.cs b/src/ClaudeDo.Worker/Online/OnlineTokenStore.cs
deleted file mode 100644
index b07d9697..00000000
--- a/src/ClaudeDo.Worker/Online/OnlineTokenStore.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Runtime.Versioning;
-using System.Security.Cryptography;
-using System.Text;
-using ClaudeDo.Data;
-
-namespace ClaudeDo.Worker.Online;
-
-///
-/// Persists the Zitadel refresh token encrypted with DPAPI (CurrentUser scope).
-/// Windows-only; the file lives at ~/.claudeDo/online-inbox.token.
-///
-[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);
- }
-}
diff --git a/src/ClaudeDo.Worker/Online/ZitadelAuthProvider.cs b/src/ClaudeDo.Worker/Online/ZitadelAuthProvider.cs
index 3cee47e3..0b66824f 100644
--- a/src/ClaudeDo.Worker/Online/ZitadelAuthProvider.cs
+++ b/src/ClaudeDo.Worker/Online/ZitadelAuthProvider.cs
@@ -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 _logger;
@@ -29,12 +29,12 @@ public sealed class ZitadelAuthProvider : IOnlineAuthProvider
public ZitadelAuthProvider(
IHttpClientFactory httpClientFactory,
- OnlineTokenStore tokenStore,
+ OnlineRefreshTokenStore tokenStore,
OnlineInboxConfig config,
ILogger logger)
{
_httpClientFactory = httpClientFactory;
- _tokenStore = tokenStore;
+ _tokenStore = tokenStore.Store;
_config = config;
_logger = logger;
}
diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs
index 64850d0b..5126053c 100644
--- a/src/ClaudeDo.Worker/Program.cs
+++ b/src/ClaudeDo.Worker/Program.cs
@@ -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()
.WithTools();
-// 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();
+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)
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/ClearMyDayHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/ClearMyDayHubTests.cs
index 7e4ac855..b57125a9 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/ClearMyDayHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/ClearMyDayHubTests.cs
@@ -20,7 +20,7 @@ public sealed class ClearMyDayHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/DeleteTaskHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/DeleteTaskHubTests.cs
index a0ff1339..de3d4cbd 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/DeleteTaskHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/DeleteTaskHubTests.cs
@@ -25,7 +25,7 @@ public sealed class DeleteTaskHubTests : IDisposable
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
built.State,
null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/ListConfigHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/ListConfigHubTests.cs
index d3a2bc6d..373ed443 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/ListConfigHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/ListConfigHubTests.cs
@@ -23,7 +23,7 @@ public sealed class ListConfigHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, factory,
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/MergeHelperTaskHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/MergeHelperTaskHubTests.cs
index 35038e78..ed3535c3 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/MergeHelperTaskHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/MergeHelperTaskHubTests.cs
@@ -71,7 +71,7 @@ public sealed class MergeHelperTaskHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, null!, factory, null!, null!, null!,
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!,
logBuffer: null, interactiveLaunchSpec: interactiveLaunchSpec, worktreeManager: wtManager, git: git,
interactiveReviewSubmission: reviewSubmission);
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs
index c654fd15..99f0b432 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs
@@ -1,3 +1,4 @@
+using ClaudeDo.Worker;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Online;
@@ -21,17 +22,17 @@ public sealed class OnlineInboxHubTests : IDisposable
try { File.Delete(_tokenPath); } catch { }
}
- private (WorkerHub hub, OnlineInboxConfig inboxCfg, OnlineTokenStore store) CreateHub(
- OnlineInboxConfig? inboxCfg = null, OnlineTokenStore? store = null)
+ private (WorkerHub hub, OnlineInboxConfig inboxCfg, DpapiTokenStore store) CreateHub(
+ OnlineInboxConfig? inboxCfg = null, DpapiTokenStore? store = null)
{
var cfg = new WorkerConfig();
inboxCfg ??= cfg.OnlineInbox;
- store ??= new OnlineTokenStore(_tokenPath);
+ store ??= new DpapiTokenStore(_tokenPath);
var broadcaster = new HubBroadcaster(new CapturingHubContext());
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, null!,
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- cfg, inboxCfg, store, new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
+ cfg, inboxCfg, new OnlineRefreshTokenStore(store), new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
return (hub, inboxCfg, store);
@@ -95,7 +96,7 @@ public sealed class OnlineInboxHubTests : IDisposable
var cfg = new WorkerConfig();
// point SaveOnlineInbox to our temp file
var inboxCfg = cfg.OnlineInbox;
- var store = new OnlineTokenStore(_tokenPath);
+ var store = new DpapiTokenStore(_tokenPath);
var broadcaster = new HubBroadcaster(new CapturingHubContext());
// Patch cfg to save to our temp path by writing an initial file there
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs
index fad6a070..1bcbd7e9 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs
@@ -56,7 +56,7 @@ public sealed class PlanningHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, null!, _db.CreateFactory(), null!, null!, null!,
_planning, _launcher, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(_proxy);
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/RepoImportFoldersHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/RepoImportFoldersHubTests.cs
index 9daf46c1..ec902b39 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/RepoImportFoldersHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/RepoImportFoldersHubTests.cs
@@ -18,7 +18,7 @@ public sealed class RepoImportFoldersHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/SessionSkillsHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/SessionSkillsHubTests.cs
index afae35f4..ec63f3d8 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/SessionSkillsHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/SessionSkillsHubTests.cs
@@ -51,7 +51,7 @@ public sealed class SessionSkillsHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), registry);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/SetTaskStatusHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/SetTaskStatusHubTests.cs
index ccdbb563..382c8b88 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/SetTaskStatusHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/SetTaskStatusHubTests.cs
@@ -25,7 +25,7 @@ public sealed class SetTaskStatusHubTests : IDisposable
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
built.State,
null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/TaskDoneDequeueHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/TaskDoneDequeueHubTests.cs
index 32571b56..8368199d 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/TaskDoneDequeueHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/TaskDoneDequeueHubTests.cs
@@ -40,7 +40,7 @@ public sealed class TaskDoneDequeueHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, null!, factory, null!, null!, null!,
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(_proxy);
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/TaskUsageHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/TaskUsageHubTests.cs
index 2fc4d7a7..83feaa23 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/TaskUsageHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/TaskUsageHubTests.cs
@@ -19,7 +19,7 @@ public sealed class TaskUsageHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/WorkerBuildInfoHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/WorkerBuildInfoHubTests.cs
index 64624422..c4a3d4db 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/WorkerBuildInfoHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/WorkerBuildInfoHubTests.cs
@@ -25,7 +25,7 @@ public sealed class WorkerBuildInfoHubTests
var hub = new WorkerHub(
null!, null!, null!, null!, null!, null!,
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
// No assertion on the actual SHA value (depends on the build environment) — just that
diff --git a/tests/ClaudeDo.Worker.Tests/Hub/WorktreeStateHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/WorktreeStateHubTests.cs
index ee803906..9207103d 100644
--- a/tests/ClaudeDo.Worker.Tests/Hub/WorktreeStateHubTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Hub/WorktreeStateHubTests.cs
@@ -20,7 +20,7 @@ public sealed class WorktreeStateHubTests : IDisposable
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
- null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
+ null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
diff --git a/tests/ClaudeDo.Worker.Tests/Online/OnlineTokenStoreTests.cs b/tests/ClaudeDo.Worker.Tests/Online/OnlineTokenStoreTests.cs
deleted file mode 100644
index 404351bc..00000000
--- a/tests/ClaudeDo.Worker.Tests/Online/OnlineTokenStoreTests.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using ClaudeDo.Worker.Online;
-
-namespace ClaudeDo.Worker.Tests.Online;
-
-public sealed class OnlineTokenStoreTests : IDisposable
-{
- private readonly string _tokenPath = Path.Combine(Path.GetTempPath(), $"online_token_{Guid.NewGuid():N}.bin");
-
- public void Dispose()
- {
- try { File.Delete(_tokenPath); } catch { }
- }
-
- [Fact]
- public void Save_Read_RoundTrips()
- {
- if (!OperatingSystem.IsWindows()) return; // DPAPI is Windows-only
-
- var store = new OnlineTokenStore(_tokenPath);
- store.Save("my-refresh-token");
- var result = store.Read();
- Assert.Equal("my-refresh-token", result);
- }
-
- [Fact]
- public void Clear_Removes_Token()
- {
- if (!OperatingSystem.IsWindows()) return;
-
- var store = new OnlineTokenStore(_tokenPath);
- store.Save("token");
- store.Clear();
- Assert.Null(store.Read());
- }
-
- [Fact]
- public void Read_WhenFileAbsent_Returns_Null()
- {
- if (!OperatingSystem.IsWindows()) return;
-
- var store = new OnlineTokenStore(_tokenPath);
- Assert.Null(store.Read());
- }
-
- [Fact]
- public void Clear_WhenFileAbsent_DoesNotThrow()
- {
- if (!OperatingSystem.IsWindows()) return;
-
- var store = new OnlineTokenStore(_tokenPath);
- store.Clear(); // no exception expected
- }
-}
diff --git a/tests/ClaudeDo.Worker.Tests/Online/ZitadelAuthProviderTests.cs b/tests/ClaudeDo.Worker.Tests/Online/ZitadelAuthProviderTests.cs
index c8c502d9..8ef8f2cc 100644
--- a/tests/ClaudeDo.Worker.Tests/Online/ZitadelAuthProviderTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Online/ZitadelAuthProviderTests.cs
@@ -1,6 +1,7 @@
using System.Net;
using System.Text;
using System.Text.Json;
+using ClaudeDo.Worker;
using ClaudeDo.Worker.Online;
using Microsoft.Extensions.Logging.Abstractions;
@@ -8,8 +9,8 @@ namespace ClaudeDo.Worker.Tests.Online;
///
/// Tests for using a stub .
-/// Token-store tests use a real temp-dir (DPAPI) and are
-/// Windows-only, consistent with .
+/// Token-store tests use a real temp-dir (DPAPI) and are
+/// Windows-only.
///
public sealed class ZitadelAuthProviderTests : IDisposable
{
@@ -86,14 +87,14 @@ public sealed class ZitadelAuthProviderTests : IDisposable
},
};
- private (ZitadelAuthProvider Provider, StubHandler Handler, OnlineTokenStore Store) Build()
+ private (ZitadelAuthProvider Provider, StubHandler Handler, DpapiTokenStore Store) Build()
{
var handler = new StubHandler();
var factory = new StubHttpClientFactory(handler);
- var store = new OnlineTokenStore(_tokenPath);
+ var store = new DpapiTokenStore(_tokenPath);
var config = MakeConfig();
var provider = new ZitadelAuthProvider(
- factory, store, config, NullLogger.Instance);
+ factory, new OnlineRefreshTokenStore(store), config, NullLogger.Instance);
return (provider, handler, store);
}
diff --git a/tests/ClaudeDo.Worker.Tests/Tickets/DpapiTokenStoreTests.cs b/tests/ClaudeDo.Worker.Tests/Tickets/DpapiTokenStoreTests.cs
new file mode 100644
index 00000000..2a62f958
--- /dev/null
+++ b/tests/ClaudeDo.Worker.Tests/Tickets/DpapiTokenStoreTests.cs
@@ -0,0 +1,45 @@
+using ClaudeDo.Worker;
+
+namespace ClaudeDo.Worker.Tests.Tickets;
+
+public sealed class DpapiTokenStoreTests : IDisposable
+{
+ private readonly string _dir = Path.Combine(Path.GetTempPath(), "cdo-tok-" + Guid.NewGuid().ToString("N"));
+
+ [Fact]
+ public void Save_and_read_round_trip()
+ {
+ var store = new DpapiTokenStore(Path.Combine(_dir, "ticket.pat"));
+ store.Save("tsp_secret");
+ Assert.Equal("tsp_secret", store.Read());
+ }
+
+ [Fact]
+ public void Read_returns_null_when_missing()
+ => Assert.Null(new DpapiTokenStore(Path.Combine(_dir, "nope.pat")).Read());
+
+ [Fact]
+ public void Two_stores_with_different_files_do_not_share_state()
+ {
+ var a = new DpapiTokenStore(Path.Combine(_dir, "a.token"));
+ var b = new DpapiTokenStore(Path.Combine(_dir, "b.token"));
+ a.Save("aaa");
+ b.Save("bbb");
+ Assert.Equal("aaa", a.Read());
+ Assert.Equal("bbb", b.Read());
+ }
+
+ [Fact]
+ public void Clear_removes_the_file()
+ {
+ var store = new DpapiTokenStore(Path.Combine(_dir, "c.token"));
+ store.Save("x");
+ store.Clear();
+ Assert.Null(store.Read());
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_dir)) Directory.Delete(_dir, recursive: true);
+ }
+}