mirror of
https://github.com/saltbo/agent-kanban.git
synced 2026-09-19 09:01:47 +08:00
96fdd70d2c
* feat(api)!: redesign task resource operations
Publish the complete resource-oriented OpenAPI contract, enforce scope-based authorization at the HTTP boundary, and replace lifecycle command routes with Task PATCH and nested Claim/Event resources.
Add server-side Task CAS, migration coverage, updated Agent Skills, and browser/API regression tests.
BREAKING CHANGE: remove the legacy task lifecycle endpoints and move bounded waits to /tasks/{taskId}/events.
* refactor(tasks)!: make claims create-only
Remove Task Claim retrieval and deletion routes, the release scope, live deletion logic, and release workflow guidance.
Keep historical release records readable for migration and audit compatibility.
BREAKING CHANGE: remove GET and DELETE /tasks/{taskId}/claims/{claimId} and the task:release scope.
* test(ci): align actor and projection contracts
Cover machine and service Task Action writes and update Agent detail fixtures to the canonical assignedTo query and paged lowerCamelCase response.
* fix(api): preserve merge patch fields and consume browser pagination
* test(ui): include terminal pagination in agent task fixture
---------
Co-authored-by: jarvis <jarvis@agents.realmroot.dev>
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
import {
|
|
AuthError,
|
|
authenticateRealmrootToken,
|
|
authenticateWebSession,
|
|
type RealmrootPrincipal,
|
|
type ResourceScope,
|
|
validateCsrfToken,
|
|
} from "@server/auth/realmroot";
|
|
import type { Env } from "@server/env";
|
|
import { isPublishedV2Operation, v2Problem } from "@server/http/middleware/v2Contract";
|
|
import type { Context, Next } from "hono";
|
|
|
|
export async function authenticationMiddleware(c: Context<{ Bindings: Env }>, next: Next) {
|
|
try {
|
|
const principal = c.req.header("authorization") ? await authenticateRealmrootToken(c) : await authenticateWebSession(c);
|
|
if (!principal) return authenticationFailure(c, "Authentication required");
|
|
c.set("principal", principal);
|
|
c.set("ownerId", principal.tenantId);
|
|
return next();
|
|
} catch (error) {
|
|
if (error instanceof AuthError) return authenticationFailure(c, error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function csrfProtectionMiddleware(c: Context<{ Bindings: Env }>, next: Next) {
|
|
const principal = c.get("principal");
|
|
if (principal.source !== "session" || isSafeMethod(c.req.method)) return next();
|
|
const session = c.get("session");
|
|
if (!session || !(await validateCsrfToken(c.req.header("x-csrf-token"), session.csrfToken))) {
|
|
return authorizationFailure(c, "Invalid CSRF token");
|
|
}
|
|
return next();
|
|
}
|
|
|
|
export async function principalProvisioningMiddleware(c: Context<{ Bindings: Env }>, next: Next) {
|
|
const principal = c.get("principal");
|
|
if (principal.source === "token") await ensureTokenPrincipal(c, principal);
|
|
await next();
|
|
}
|
|
|
|
export function authorizeScope(scope: ResourceScope) {
|
|
return async (c: Context<{ Bindings: Env }>, next: Next) => {
|
|
if (!c.get("principal").scopes.includes(scope)) return authorizationFailure(c, `Missing scope: ${scope}`);
|
|
await next();
|
|
};
|
|
}
|
|
|
|
async function ensureTokenPrincipal(c: Context<{ Bindings: Env }>, principal: RealmrootPrincipal): Promise<void> {
|
|
const statements = [
|
|
c.env.DB.prepare(
|
|
`INSERT INTO realmroot_tenants (id) VALUES (?)
|
|
ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at`,
|
|
).bind(principal.tenantId),
|
|
];
|
|
if (principal.type === "human") {
|
|
statements.push(
|
|
c.env.DB.prepare(
|
|
`INSERT INTO realmroot_tenant_members (tenant_id, subject_id, role)
|
|
VALUES (?, ?, 'member')
|
|
ON CONFLICT(tenant_id, subject_id) DO UPDATE SET updated_at = excluded.updated_at`,
|
|
).bind(principal.tenantId, principal.subjectId),
|
|
);
|
|
}
|
|
await c.env.DB.batch(statements);
|
|
}
|
|
|
|
function authenticationFailure(c: Context<{ Bindings: Env }>, message: string): Response {
|
|
if (isPublishedV2Operation(c.req.method, c.req.path)) {
|
|
c.header("WWW-Authenticate", 'DPoP realm="agent-kanban"');
|
|
return v2Problem(c, 401, "authentication-required", "Authentication required", message);
|
|
}
|
|
return c.json({ error: { code: "UNAUTHORIZED", message } }, 401);
|
|
}
|
|
|
|
function authorizationFailure(c: Context<{ Bindings: Env }>, message: string): Response {
|
|
if (isPublishedV2Operation(c.req.method, c.req.path)) {
|
|
return v2Problem(c, 403, "permission-denied", "Permission denied", message);
|
|
}
|
|
return c.json({ error: { code: "FORBIDDEN", message } }, 403);
|
|
}
|
|
|
|
function isSafeMethod(method: string): boolean {
|
|
return method === "GET" || method === "HEAD" || method === "OPTIONS";
|
|
}
|