/** * Authentication middleware for oRPC server * * Handles authentication via: * - Session cookie (rev.session_token) - for browser clients * - API key header (x-api-key) - for CLI and programmatic access */ import type { APIContext, AuthenticatedContext, Session, SessionUser, } from "../context.js"; import { ORPCError } from "@orpc/server"; import { COOKIE_NAMES, getCookie } from "../utils/cookies.js"; import { hashToken } from "../utils/crypto.js"; /** * Create the auth middleware function * This returns a middleware handler that can be used with oRPC procedures */ export const createAuthMiddleware = () => { return async ({ context, next, }: { context: APIContext; next: (opts: { context: Omit; }) => Promise; }) => { const { db, reqHeaders } = context; // Try session cookie first let tokenHash: string | undefined; const sessionToken = getCookie(reqHeaders, COOKIE_NAMES.SESSION_TOKEN); if (sessionToken) { tokenHash = hashToken(sessionToken); } // Fall back to API key header (for CLI) const apiKey = reqHeaders.get("x-api-key"); if (!tokenHash && apiKey) { tokenHash = hashToken(apiKey); } if (!tokenHash) { throw new ORPCError("UNAUTHORIZED", { message: "No session or API key" }); } // Look up session (check not expired and not revoked) const session = await db .selectFrom("sessions") .where("token_hash", "=", tokenHash) .where("expires_at", ">", new Date()) .where("revoked_at", "is", null) .selectAll() .executeTakeFirst(); // Fall back to API token if no session found const apiToken = !session ? await db .selectFrom("api_tokens") .where("token_hash", "=", tokenHash) .where("expires_at", ">", new Date()) .selectAll() .executeTakeFirst() : undefined; const userId = session?.user_id ?? apiToken?.user_id; if (!userId) { throw new ORPCError("UNAUTHORIZED", { message: "Invalid or expired token", }); } // Update last_used_at for API tokens if (apiToken) { await db .updateTable("api_tokens") .set({ last_used_at: new Date() }) .where("id", "=", apiToken.id) .execute(); } // Fetch user details const user = await db .selectFrom("users") .where("id", "=", userId) .select([ "id", "email", "display_name", "email_verified_at", "is_superuser", ]) .executeTakeFirst(); if (!user) { throw new ORPCError("UNAUTHORIZED", { message: "User not found", }); } const sessionUser: SessionUser = { id: user.id, email: user.email, displayName: user.display_name, emailVerifiedAt: user.email_verified_at, isSuperuser: user.is_superuser, }; const sessionInfo: Session = session ? { id: Number(session.id), trustedMode: session.trusted_mode, createdAt: session.created_at, } : { // For API token auth, create a synthetic session object // We know apiToken exists because userId came from it id: 0, trustedMode: true, createdAt: apiToken?.created_at ?? new Date(), }; return next({ context: { user: sessionUser, session: sessionInfo, }, }); }; }; /** * Middleware to require superuser access */ export const createSuperuserMiddleware = () => { return async ({ context, next, }: { context: AuthenticatedContext; next: () => Promise; }) => { if (!context.user.isSuperuser) { throw new ORPCError("FORBIDDEN", { message: "Superuser access required", }); } return next(); }; };