Files
paperclip/server/src/middleware/auth.ts
T
Forgotten c09037ffad Implement agent hiring, approval workflows, config revisions, LLM reflection, and sidebar badges
Agent management: hire endpoint with permission gates and pending_approval status,
config revision tracking with rollback, agent duplicate route, permission CRUD.
Block pending_approval agents from auth, heartbeat, and assignments.

Approvals: revision request/resubmit flow, approval comments CRUD, issue-approval
linking, auto-wake agents on approval decisions with context snapshot.

Costs: per-agent breakdown, period filtering (month/week/day/all), cost by agent
list endpoint.

Adapters: agentConfigurationDoc on all adapters, /llms/agent-configuration.txt
reflection routes. Inject PAPERCLIP_APPROVAL_ID, PAPERCLIP_APPROVAL_STATUS,
PAPERCLIP_LINKED_ISSUE_IDS into adapter environments.

Sidebar badges endpoint for pending approval/inbox counts. Dashboard and company
settings extensions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:02:41 -06:00

103 lines
2.5 KiB
TypeScript

import { createHash } from "node:crypto";
import type { RequestHandler } from "express";
import { and, eq, isNull } from "drizzle-orm";
import type { Db } from "@paperclip/db";
import { agentApiKeys, agents } from "@paperclip/db";
import { verifyLocalAgentJwt } from "../agent-auth-jwt.js";
function hashToken(token: string) {
return createHash("sha256").update(token).digest("hex");
}
export function actorMiddleware(db: Db): RequestHandler {
return async (req, _res, next) => {
req.actor = { type: "board", userId: "board" };
const runIdHeader = req.header("x-paperclip-run-id");
const authHeader = req.header("authorization");
if (!authHeader?.toLowerCase().startsWith("bearer ")) {
if (runIdHeader) req.actor.runId = runIdHeader;
next();
return;
}
const token = authHeader.slice("bearer ".length).trim();
if (!token) {
next();
return;
}
const tokenHash = hashToken(token);
const key = await db
.select()
.from(agentApiKeys)
.where(and(eq(agentApiKeys.keyHash, tokenHash), isNull(agentApiKeys.revokedAt)))
.then((rows) => rows[0] ?? null);
if (!key) {
const claims = verifyLocalAgentJwt(token);
if (!claims) {
next();
return;
}
const agentRecord = await db
.select()
.from(agents)
.where(eq(agents.id, claims.sub))
.then((rows) => rows[0] ?? null);
if (!agentRecord || agentRecord.companyId !== claims.company_id) {
next();
return;
}
if (agentRecord.status === "terminated" || agentRecord.status === "pending_approval") {
next();
return;
}
req.actor = {
type: "agent",
agentId: claims.sub,
companyId: claims.company_id,
keyId: undefined,
runId: runIdHeader || undefined,
};
next();
return;
}
await db
.update(agentApiKeys)
.set({ lastUsedAt: new Date() })
.where(eq(agentApiKeys.id, key.id));
const agentRecord = await db
.select()
.from(agents)
.where(eq(agents.id, key.agentId))
.then((rows) => rows[0] ?? null);
if (!agentRecord || agentRecord.status === "terminated" || agentRecord.status === "pending_approval") {
next();
return;
}
req.actor = {
type: "agent",
agentId: key.agentId,
companyId: key.companyId,
keyId: key.id,
runId: runIdHeader || undefined,
};
next();
};
}
export function requireBoard(req: Express.Request) {
return req.actor.type === "board";
}