mirror of
https://github.com/khoaliber/paperclip.git
synced 2026-04-19 17:14:40 +00:00
Wire up Better Auth for session-based authentication. Add actor middleware that resolves local_trusted mode to an implicit board actor and authenticated mode to Better Auth sessions. Add access service with membership, permission, invite, and join-request management. Register access routes for member/invite/ join-request CRUD. Update health endpoint to report deployment mode and bootstrap status. Enforce tasks:assign and agents:create permissions in issue and agent routes. Add deployment mode validation at startup with guardrails (loopback-only for local_trusted, auth config required for authenticated). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import type { Request, RequestHandler } from "express";
|
|
import { betterAuth } from "better-auth";
|
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
import { toNodeHandler } from "better-auth/node";
|
|
import type { Db } from "@paperclip/db";
|
|
import {
|
|
authAccounts,
|
|
authSessions,
|
|
authUsers,
|
|
authVerifications,
|
|
} from "@paperclip/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 headersFromExpressRequest(req: Request): Headers {
|
|
const headers = new Headers();
|
|
for (const [key, raw] of Object.entries(req.headers)) {
|
|
if (!raw) continue;
|
|
if (Array.isArray(raw)) {
|
|
for (const value of raw) headers.append(key, value);
|
|
continue;
|
|
}
|
|
headers.set(key, raw);
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
export function createBetterAuthInstance(db: Db, config: Config): 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 authConfig = {
|
|
baseURL: baseUrl,
|
|
secret,
|
|
database: drizzleAdapter(db, {
|
|
provider: "pg",
|
|
schema: {
|
|
user: authUsers,
|
|
session: authSessions,
|
|
account: authAccounts,
|
|
verification: authVerifications,
|
|
},
|
|
}),
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
requireEmailVerification: 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 resolveBetterAuthSession(
|
|
auth: BetterAuthInstance,
|
|
req: Request,
|
|
): 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: headersFromExpressRequest(req),
|
|
});
|
|
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 };
|
|
}
|