| 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<typeof betterAuth>; |
|
|
| 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<string>(); |
|
|
| if (baseUrl) { |
| try { |
| trustedOrigins.add(new URL(baseUrl).origin); |
| } catch { |
| |
| } |
| } |
| 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 publicUrl = process.env.PAPERCLIP_PUBLIC_URL ?? baseUrl; |
| const isHttpOnly = publicUrl ? publicUrl.startsWith("http://") : false; |
|
|
| 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, |
| }, |
| ...(isHttpOnly ? { advanced: { useSecureCookies: 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<BetterAuthSessionResult | null> { |
| const api = (auth as unknown as { api?: { getSession?: (input: unknown) => Promise<unknown> } }).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<BetterAuthSessionResult | null> { |
| return resolveBetterAuthSessionFromHeaders(auth, headersFromExpressRequest(req)); |
| } |
|
|