import type { Request, RequestHandler } from "express"; import type { IncomingHttpHeaders } from "node:http"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { toNodeHandler } from "better-auth/node"; import type { Db } from "@paperclipai/db"; import { authAccounts, authSessions, authUsers, authVerifications, } from "@paperclipai/db"; import type { Config } from "../config.js"; export type BetterAuthSessionUser = { id: string; email?: string | null; name?: string | null; }; export type BetterAuthSessionResult = { session: { id: string; userId: string } | null; user: BetterAuthSessionUser | null; }; type BetterAuthInstance = ReturnType; function headersFromNodeHeaders(rawHeaders: IncomingHttpHeaders): Headers { const headers = new Headers(); for (const [key, raw] of Object.entries(rawHeaders)) { if (!raw) continue; if (Array.isArray(raw)) { for (const value of raw) headers.append(key, value); continue; } headers.set(key, raw); } return headers; } function headersFromExpressRequest(req: Request): Headers { return headersFromNodeHeaders(req.headers); } export function deriveAuthTrustedOrigins(config: Config): string[] { const baseUrl = config.authBaseUrlMode === "explicit" ? config.authPublicBaseUrl : undefined; const trustedOrigins = new Set(); if (baseUrl) { try { trustedOrigins.add(new URL(baseUrl).origin); } catch { // Better Auth will surface invalid base URL separately. } } if (config.deploymentMode === "authenticated") { for (const hostname of config.allowedHostnames) { const trimmed = hostname.trim().toLowerCase(); if (!trimmed) continue; trustedOrigins.add(`https://${trimmed}`); trustedOrigins.add(`http://${trimmed}`); } } return Array.from(trustedOrigins); } export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins?: string[]): BetterAuthInstance { const baseUrl = config.authBaseUrlMode === "explicit" ? config.authPublicBaseUrl : undefined; const secret = process.env.BETTER_AUTH_SECRET ?? process.env.PAPERCLIP_AGENT_JWT_SECRET ?? "paperclip-dev-secret"; const effectiveTrustedOrigins = trustedOrigins ?? deriveAuthTrustedOrigins(config); const authConfig = { baseURL: baseUrl, secret, trustedOrigins: effectiveTrustedOrigins, database: drizzleAdapter(db, { provider: "pg", schema: { user: authUsers, session: authSessions, account: authAccounts, verification: authVerifications, }, }), emailAndPassword: { enabled: true, requireEmailVerification: false, disableSignUp: config.authDisableSignUp ?? false, }, }; if (!baseUrl) { delete (authConfig as { baseURL?: string }).baseURL; } return betterAuth(authConfig); } export function createBetterAuthHandler(auth: BetterAuthInstance): RequestHandler { const handler = toNodeHandler(auth); return (req, res, next) => { void Promise.resolve(handler(req, res)).catch(next); }; } export async function resolveBetterAuthSessionFromHeaders( auth: BetterAuthInstance, headers: Headers, ): Promise { const api = (auth as unknown as { api?: { getSession?: (input: unknown) => Promise } }).api; if (!api?.getSession) return null; const sessionValue = await api.getSession({ headers, }); if (!sessionValue || typeof sessionValue !== "object") return null; const value = sessionValue as { session?: { id?: string; userId?: string } | null; user?: { id?: string; email?: string | null; name?: string | null } | null; }; const session = value.session?.id && value.session.userId ? { id: value.session.id, userId: value.session.userId } : null; const user = value.user?.id ? { id: value.user.id, email: value.user.email ?? null, name: value.user.name ?? null, } : null; if (!session || !user) return null; return { session, user }; } export async function resolveBetterAuthSession( auth: BetterAuthInstance, req: Request, ): Promise { return resolveBetterAuthSessionFromHeaders(auth, headersFromExpressRequest(req)); }