Files
ClaudeDo/src/ClaudeDo.Worker/Online/OnlineTokenStore.cs
mika kuns 1ac9ced0bd feat(worker): Online Inbox sync engine (Phase 1)
Optional, opt-in (online_inbox.enabled, default false → zero network).
Worker-side reconcile loop: pull web-created tasks down as Idle, push the
list catalog and the Idle backlog mirror up. Auth behind IOnlineAuthProvider
(StaticTokenAuthProvider default; ZitadelAuthProvider stubbed for Phase 2).
DPAPI refresh-token store. 35 tests, no real network/Zitadel/Claude.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:55:20 +02:00

55 lines
1.5 KiB
C#

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 ~/.todo-app/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);
}
}