// src/server/middleware/auth.ts // // Supabase JWT verification + license gating for the paid API. // // The central Supabase project issues the session JWT; this backend verifies // it against the project's PUBLIC JWKS endpoint (no shared database needed). // License rows are read from the central project via its service-role REST // API (server-side only). // // Enforcement is OFF by default (LICENSE_ENFORCEMENT != "true") so existing // open/demo deployments keep working. Turn it on at go-live: // // SUPABASE_URL=https://ciytvulujiqemqljablq.supabase.co // SUPABASE_SERVICE_ROLE_KEY= // LICENSE_ENFORCEMENT=true // // Frontend equivalent lives in the FortyDN website repo (src/lib/productApi.ts). // Ported from BRIDGE Modul-X (src/server/middleware/auth.ts) — same contract. import type { Request, Response, NextFunction } from "express"; import { createRemoteJWKSet, jwtVerify } from "jose"; export const CENTRAL_SUPABASE_URL = process.env.SUPABASE_URL || "https://ciytvulujiqemqljablq.supabase.co"; const JWKS_URL = new URL(`${CENTRAL_SUPABASE_URL}/auth/v1/.well-known/jwks.json`); let jwks: ReturnType | null = null; function getJwks() { if (!jwks) jwks = createRemoteJWKSet(JWKS_URL); return jwks; } function enforcementOn(): boolean { return process.env.LICENSE_ENFORCEMENT === "true"; } export interface AuthClaims { userId: string; email?: string; } function bearerToken(req: Request): string | null { const header = req.headers.authorization || ""; if (!header.startsWith("Bearer ")) return null; const token = header.slice(7).trim(); return token.length > 0 ? token : null; } export async function verifySupabaseJwt(token: string): Promise { const { payload } = await jwtVerify(token, getJwks(), { audience: "authenticated", }); const sub = payload.sub; if (!sub) throw new Error("JWT has no sub claim"); return { userId: sub, email: typeof payload.email === "string" ? payload.email : undefined, }; } /** Combos that unlock Modul-S (mirrors the website catalog). */ const MODULS_GRANTS = new Set([ "modul-s", "combo-validate-send", "combo-full-bridge", ]); export async function hasModulSLicense(userId: string): Promise { const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!serviceKey) { console.warn("[auth] SUPABASE_SERVICE_ROLE_KEY not set — cannot check licenses."); return false; } const params = new URLSearchParams({ user_id: `eq.${userId}`, status: "eq.active", select: "product", }); const res = await fetch(`${CENTRAL_SUPABASE_URL}/rest/v1/licenses?${params}`, { headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}`, }, }); if (!res.ok) { const body = await res.text().catch(() => ""); // Missing table (migrations not applied yet) => fail closed with a hint. console.warn(`[auth] license lookup HTTP ${res.status}: ${body.slice(0, 200)}`); return false; } const rows = (await res.json()) as { product?: string }[]; return ( Array.isArray(rows) && rows.some((r) => r.product && MODULS_GRANTS.has(r.product)) ); } /** * Requires a valid Supabase JWT. No-op (with a one-time warning) while * LICENSE_ENFORCEMENT is off so demo deployments keep working. */ export function requireAuth(req: Request, res: Response, next: NextFunction) { if (!enforcementOn()) return next(); void (async () => { const token = bearerToken(req); if (!token) { return res.status(401).json({ detail: "Missing bearer token — sign in first." }); } try { const claims = await verifySupabaseJwt(token); (req as Request & { auth?: AuthClaims }).auth = claims; return next(); } catch { return res.status(401).json({ detail: "Invalid or expired token." }); } })(); } /** * Requires a valid JWT AND an active Modul-S license (direct or via combo). * No-op while LICENSE_ENFORCEMENT is off. */ export function requireModulSLicense(req: Request, res: Response, next: NextFunction) { if (!enforcementOn()) return next(); void (async () => { const token = bearerToken(req); if (!token) { return res.status(401).json({ detail: "Missing bearer token — sign in first." }); } try { const claims = await verifySupabaseJwt(token); (req as Request & { auth?: AuthClaims }).auth = claims; const ok = await hasModulSLicense(claims.userId); if (!ok) { return res .status(402) .json({ detail: "No active Modul-S license — see /pricing." }); } return next(); } catch { return res.status(401).json({ detail: "Invalid or expired token." }); } })(); }