feat(server): integrate Better Auth, access control, and deployment mode startup

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>
This commit is contained in:
Forgotten
2026-02-23 14:40:32 -06:00
parent 60d6122271
commit e1f2be7ecf
24 changed files with 1530 additions and 49 deletions

View File

@@ -1,8 +1,9 @@
import express, { Router } from "express";
import express, { Router, type Request as ExpressRequest } from "express";
import path from "node:path";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import type { Db } from "@paperclip/db";
import type { DeploymentExposure, DeploymentMode } from "@paperclip/shared";
import type { StorageService } from "./storage/types.js";
import { httpLogger, errorHandler } from "./middleware/index.js";
import { actorMiddleware } from "./middleware/auth.js";
@@ -21,21 +22,49 @@ import { dashboardRoutes } from "./routes/dashboard.js";
import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js";
import { llmRoutes } from "./routes/llms.js";
import { assetRoutes } from "./routes/assets.js";
import { accessRoutes } from "./routes/access.js";
import type { BetterAuthSessionResult } from "./auth/better-auth.js";
type UiMode = "none" | "static" | "vite-dev";
export async function createApp(db: Db, opts: { uiMode: UiMode; storageService: StorageService }) {
export async function createApp(
db: Db,
opts: {
uiMode: UiMode;
storageService: StorageService;
deploymentMode: DeploymentMode;
deploymentExposure: DeploymentExposure;
authReady: boolean;
betterAuthHandler?: express.RequestHandler;
resolveSession?: (req: ExpressRequest) => Promise<BetterAuthSessionResult | null>;
},
) {
const app = express();
app.use(express.json());
app.use(httpLogger);
app.use(actorMiddleware(db));
app.use(
actorMiddleware(db, {
deploymentMode: opts.deploymentMode,
resolveSession: opts.resolveSession,
}),
);
if (opts.betterAuthHandler) {
app.all("/api/auth/*authPath", opts.betterAuthHandler);
}
app.use(llmRoutes(db));
// Mount API routes
const api = Router();
api.use(boardMutationGuard());
api.use("/health", healthRoutes());
api.use(
"/health",
healthRoutes(db, {
deploymentMode: opts.deploymentMode,
deploymentExposure: opts.deploymentExposure,
authReady: opts.authReady,
}),
);
api.use("/companies", companyRoutes(db));
api.use(agentRoutes(db));
api.use(assetRoutes(db, opts.storageService));
@@ -48,6 +77,7 @@ export async function createApp(db: Db, opts: { uiMode: UiMode; storageService:
api.use(activityRoutes(db));
api.use(dashboardRoutes(db));
api.use(sidebarBadgeRoutes(db));
api.use(accessRoutes(db));
app.use("/api", api);
const __dirname = path.dirname(fileURLToPath(import.meta.url));