Spaces:
Paused
Paused
| import { createHash } from "node:crypto"; | |
| import type { Request, RequestHandler } from "express"; | |
| import { and, eq, isNull } from "drizzle-orm"; | |
| import type { Db } from "@paperclipai/db"; | |
| import { agentApiKeys, agents, companyMemberships, instanceUserRoles } from "@paperclipai/db"; | |
| import { verifyLocalAgentJwt } from "../agent-auth-jwt.js"; | |
| import type { DeploymentMode } from "@paperclipai/shared"; | |
| import type { BetterAuthSessionResult } from "../auth/better-auth.js"; | |
| import { logger } from "./logger.js"; | |
| import { boardAuthService } from "../services/board-auth.js"; | |
| function hashToken(token: string) { | |
| return createHash("sha256").update(token).digest("hex"); | |
| } | |
| interface ActorMiddlewareOptions { | |
| deploymentMode: DeploymentMode; | |
| resolveSession?: (req: Request) => Promise<BetterAuthSessionResult | null>; | |
| } | |
| export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHandler { | |
| const boardAuth = boardAuthService(db); | |
| return async (req, _res, next) => { | |
| req.actor = | |
| opts.deploymentMode === "local_trusted" | |
| ? { type: "board", userId: "local-board", isInstanceAdmin: true, source: "local_implicit" } | |
| : { type: "none", source: "none" }; | |
| const runIdHeader = req.header("x-paperclip-run-id"); | |
| const authHeader = req.header("authorization"); | |
| if (!authHeader?.toLowerCase().startsWith("bearer ")) { | |
| if (opts.deploymentMode === "authenticated" && opts.resolveSession) { | |
| let session: BetterAuthSessionResult | null = null; | |
| try { | |
| session = await opts.resolveSession(req); | |
| } catch (err) { | |
| logger.warn( | |
| { err, method: req.method, url: req.originalUrl }, | |
| "Failed to resolve auth session from request headers", | |
| ); | |
| } | |
| if (session?.user?.id) { | |
| const userId = session.user.id; | |
| const [roleRow, memberships] = await Promise.all([ | |
| db | |
| .select({ id: instanceUserRoles.id }) | |
| .from(instanceUserRoles) | |
| .where(and(eq(instanceUserRoles.userId, userId), eq(instanceUserRoles.role, "instance_admin"))) | |
| .then((rows) => rows[0] ?? null), | |
| db | |
| .select({ companyId: companyMemberships.companyId }) | |
| .from(companyMemberships) | |
| .where( | |
| and( | |
| eq(companyMemberships.principalType, "user"), | |
| eq(companyMemberships.principalId, userId), | |
| eq(companyMemberships.status, "active"), | |
| ), | |
| ), | |
| ]); | |
| req.actor = { | |
| type: "board", | |
| userId, | |
| companyIds: memberships.map((row) => row.companyId), | |
| isInstanceAdmin: Boolean(roleRow), | |
| runId: runIdHeader ?? undefined, | |
| source: "session", | |
| }; | |
| next(); | |
| return; | |
| } | |
| } | |
| if (runIdHeader) req.actor.runId = runIdHeader; | |
| next(); | |
| return; | |
| } | |
| const token = authHeader.slice("bearer ".length).trim(); | |
| if (!token) { | |
| next(); | |
| return; | |
| } | |
| const boardKey = await boardAuth.findBoardApiKeyByToken(token); | |
| if (boardKey) { | |
| const access = await boardAuth.resolveBoardAccess(boardKey.userId); | |
| if (access.user) { | |
| await boardAuth.touchBoardApiKey(boardKey.id); | |
| req.actor = { | |
| type: "board", | |
| userId: boardKey.userId, | |
| companyIds: access.companyIds, | |
| isInstanceAdmin: access.isInstanceAdmin, | |
| keyId: boardKey.id, | |
| runId: runIdHeader || undefined, | |
| source: "board_key", | |
| }; | |
| 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, | |
| source: "agent_jwt", | |
| }; | |
| 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, | |
| source: "agent_key", | |
| }; | |
| next(); | |
| }; | |
| } | |
| export function requireBoard(req: Express.Request) { | |
| return req.actor.type === "board"; | |
| } | |