- Added disableSignUp to authConfigSchema in config-schema.ts - Added authDisableSignUp to Config interface - Added parsing from PAPERCLIP_AUTH_DISABLE_SIGN_UP env or config file - Passed to better-auth emailAndPassword.disableSignUp When true, blocks new user registrations on public instances. Defaults to false (backward compatible). Fixes #241
144 lines
4.2 KiB
TypeScript
144 lines
4.2 KiB
TypeScript
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 {
|
|
// 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<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));
|
|
}
|