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 || claims.run_id || 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"; }