T2/Phase 2A: port auth + infrastructure routes to Hono (~8 routes)

- /api/health: DB ping + version + uptime
   - /api/auth/*: NextAuth -> Auth.js standalone (credentials + passkey)
   - /api/domains: full CRUD
   - /api/realtime: SSE with PostgreSQL LISTEN/NOTIFY
   - /mcp: JSON-RPC 2.0 (initialize, tools/list, tools/call, resources/*)

   Parent: t_e1cbd87d -> t_d8654a91 (T1)
This commit is contained in:
Hermes
2026-08-01 01:25:01 +00:00
parent fca56ab77e
commit e4a241b38f
11 changed files with 1349 additions and 42 deletions
+26
View File
@@ -0,0 +1,26 @@
import { db, sql, activityFeed } from "@project-e/db";
export interface RecordActivityParams {
actor: string;
action: string;
entityType: string;
entityId: string;
changes?: Record<string, unknown>;
workspaceId: string;
}
export async function recordActivity(params: RecordActivityParams): Promise<void> {
const { actor, action, entityType, entityId, changes, workspaceId } = params;
await db.insert(activityFeed).values({
actor,
action,
entityType,
entityId,
changes: changes ?? null,
workspaceId,
});
const payload = JSON.stringify({ type: entityType, action, id: entityId, workspace_id: workspaceId });
await sql`SELECT pg_notify(project_e_events, ${payload}::text)`;
}
+110
View File
@@ -0,0 +1,110 @@
import { createMiddleware } from "hono/factory";
import type { Context, Next } from "hono";
import { jwtVerify, SignJWT } from "jose";
import { db, users } from "@project-e/db";
import { eq } from "drizzle-orm";
const AUTH_SECRET = new TextEncoder().encode(process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "fallback-secret-change-me");
const COOKIE_NAME = "session";
export interface AuthUser {
id: string;
email: string;
name: string;
}
declare module "hono" {
interface ContextVariableMap {
user: AuthUser | null;
}
}
export async function createToken(user: { id: string; email: string; name: string }): Promise<string> {
return new SignJWT({ sub: user.id, email: user.email, name: user.name })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("7d")
.sign(AUTH_SECRET);
}
export async function verifyToken(token: string): Promise<{ id: string; email: string; name: string } | null> {
try {
const { payload } = await jwtVerify(token, AUTH_SECRET);
if (!payload.sub || !payload.email) return null;
return {
id: payload.sub as string,
email: payload.email as string,
name: (payload.name as string) || (payload.email as string),
};
} catch {
return null;
}
}
export const authMiddleware = createMiddleware(async (c: Context, next: Next) => {
const cookieHeader = c.req.header("Cookie") || "";
const cookies = Object.fromEntries(
cookieHeader.split(";").map(s => s.trim().split("=")).filter(([k]) => k).map(([k, ...v]) => [k, v.join("=")])
);
const token = cookies[COOKIE_NAME] || c.req.header("Authorization")?.replace("Bearer ", "");
if (token) {
const user = await verifyToken(token);
if (user) {
c.set("user", user);
return next();
}
}
c.set("user", null);
return next();
});
export async function requireAuth(c: Context): Promise<AuthUser> {
const user = c.get("user");
if (!user) {
throw new AuthError("Not authenticated", 401, "UNAUTHORIZED");
}
return user;
}
export async function resolveActiveDomain(user: { id: string; email: string; name?: string | null }): Promise<{ id: string; name: string; created: boolean }> {
const { domains } = await import("@project-e/db");
const { asc } = await import("drizzle-orm");
const [existing] = await db
.select({ id: domains.id, name: domains.name })
.from(domains)
.where(eq(domains.ownerId, user.id))
.orderBy(asc(domains.sortOrder), asc(domains.createdAt))
.limit(1);
if (existing) return { ...existing, created: false };
const slug = "personal-" + user.id.slice(0, 8);
const [created] = await db.insert(domains).values({
ownerId: user.id,
name: "Personal",
slug: slug,
sortOrder: 0,
}).returning({ id: domains.id, name: domains.name });
return { ...created, created: true };
}
export class AuthError extends Error {
constructor(
message: string,
public status: number = 401,
public code: string = "UNAUTHORIZED"
) {
super(message);
this.name = "AuthError";
}
}
export function createErrorResponse(code: string, message: string, status: number = 400, details?: unknown) {
return {
error: {
code,
message,
...(details !== undefined ? { details } : {}),
},
};
}