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>
55 lines
1.5 KiB
C#
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);
|
|
}
|
|
}
|