diff --git a/src/ClaudeDo.Worker/Planning/PlanningMcpContext.cs b/src/ClaudeDo.Worker/Planning/PlanningMcpContext.cs new file mode 100644 index 0000000..bf29dcd --- /dev/null +++ b/src/ClaudeDo.Worker/Planning/PlanningMcpContext.cs @@ -0,0 +1,6 @@ +namespace ClaudeDo.Worker.Planning; + +public sealed class PlanningMcpContext +{ + public required string ParentTaskId { get; init; } +} diff --git a/src/ClaudeDo.Worker/Planning/PlanningTokenAuth.cs b/src/ClaudeDo.Worker/Planning/PlanningTokenAuth.cs new file mode 100644 index 0000000..6cf7043 --- /dev/null +++ b/src/ClaudeDo.Worker/Planning/PlanningTokenAuth.cs @@ -0,0 +1,40 @@ +using ClaudeDo.Data.Repositories; +using Microsoft.AspNetCore.Http; + +namespace ClaudeDo.Worker.Planning; + +public sealed class PlanningTokenAuthMiddleware +{ + private readonly RequestDelegate _next; + + public PlanningTokenAuthMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext ctx, TaskRepository tasks) + { + if (!ctx.Request.Path.StartsWithSegments("/mcp")) + { + await _next(ctx); + return; + } + + var auth = ctx.Request.Headers["Authorization"].ToString(); + if (!auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + ctx.Response.StatusCode = 401; + await ctx.Response.WriteAsync("Missing bearer token"); + return; + } + + var token = auth.Substring("Bearer ".Length).Trim(); + var parent = await tasks.FindByPlanningTokenAsync(token, ctx.RequestAborted); + if (parent is null || parent.Status != ClaudeDo.Data.Models.TaskStatus.Planning) + { + ctx.Response.StatusCode = 401; + await ctx.Response.WriteAsync("Invalid or expired planning token"); + return; + } + + ctx.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id }; + await _next(ctx); + } +}