feat(online-inbox): carry ownerId on sync to prepare for multi-user

Plumb a per-resource owner (Zitadel sub) through the sync contract without
enforcing isolation client-side — the server stays the authority.

- Dtos: add optional ownerId to RemoteList/RemoteTask/MirrorTask
- JwtClaims: decode the sub claim from the access token (never throws)
- OnlineSyncService: stamp ownerId on pushed lists + mirror; defensively skip
  pulled tasks owned by a different user (unowned tasks still sync, so
  single-user behavior is unchanged)
- docs: contract documents ownerId + multi-user readiness

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-10 13:57:39 +02:00
parent 23c3065f20
commit cee051bb6d
6 changed files with 207 additions and 12 deletions

View File

@@ -0,0 +1,40 @@
using System.Text;
using ClaudeDo.Worker.Online;
namespace ClaudeDo.Worker.Tests.Online;
public sealed class JwtClaimsTests
{
// Builds a fake JWT (header.payload.signature) carrying the given JSON payload.
internal static string MakeToken(string payloadJson)
{
static string B64Url(string s) =>
Convert.ToBase64String(Encoding.UTF8.GetBytes(s))
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
return $"{B64Url("{\"alg\":\"RS256\"}")}.{B64Url(payloadJson)}.sig";
}
[Fact]
public void GetSubject_ReturnsSub()
{
var token = MakeToken("{\"sub\":\"user-123\",\"email\":\"a@b.c\"}");
Assert.Equal("user-123", JwtClaims.GetSubject(token));
}
[Fact]
public void GetSubject_Null_WhenNoSub()
{
var token = MakeToken("{\"email\":\"a@b.c\"}");
Assert.Null(JwtClaims.GetSubject(token));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("not-a-jwt")]
[InlineData("opaque.token")]
public void GetSubject_Null_ForUnparseableInput(string? token)
{
Assert.Null(JwtClaims.GetSubject(token));
}
}