/** * SDK Server — Auth locale Itinova * Remplace le flow Manus OAuth par une authentification locale email/password + JWT. * Le cookie de session contient un JWT signé avec JWT_SECRET. */ import { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; import { ForbiddenError } from "@shared/_core/errors"; import { parse as parseCookieHeader } from "cookie"; import type { Request } from "express"; import { SignJWT, jwtVerify } from "jose"; import type { User } from "../../drizzle/schema"; import * as db from "../db"; import { ENV } from "./env"; const isNonEmptyString = (value: unknown): value is string => typeof value === "string" && value.length > 0; export type SessionPayload = { userId: number; login: string; role: string; }; /** Result of `sdk.authenticateRequest`. */ export type AuthenticatedUser = User & { taskUid?: string; isCron?: boolean; }; class SDKServer { private getSessionSecret() { const secret = ENV.cookieSecret; return new TextEncoder().encode(secret); } async createSessionToken( userId: number, login: string, role: string, options: { expiresInMs?: number } = {} ): Promise { const issuedAt = Date.now(); const expiresInMs = options.expiresInMs ?? ONE_YEAR_MS; const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1000); const secretKey = this.getSessionSecret(); return new SignJWT({ userId, login, role }) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setExpirationTime(expirationSeconds) .sign(secretKey); } async verifySession( cookieValue: string | undefined | null ): Promise { if (!cookieValue) { return null; } try { const secretKey = this.getSessionSecret(); const { payload } = await jwtVerify(cookieValue, secretKey, { algorithms: ["HS256"], }); const { userId, login, role } = payload as Record; if (!userId || !isNonEmptyString(login) || !isNonEmptyString(role)) { return null; } return { userId: userId as number, login, role }; } catch { return null; } } private parseCookies(cookieHeader: string | undefined) { if (!cookieHeader) return new Map(); const parsed = parseCookieHeader(cookieHeader); return new Map(Object.entries(parsed)); } async authenticateRequest(req: Request): Promise { const cookies = this.parseCookies(req.headers.cookie); const sessionCookie = cookies.get(COOKIE_NAME); const session = await this.verifySession(sessionCookie); if (!session) { throw ForbiddenError("Invalid session cookie"); } const user = await db.getUserById(session.userId); if (!user || !user.isActive) { throw ForbiddenError("User not found or inactive"); } return user as AuthenticatedUser; } } export const sdk = new SDKServer();