/** * JWKS-Client: lädt die öffentlichen Hub-Schlüssel und cached sie. * Tools validieren Hub-Tokens gegen diesen Key (RS256) – kein geteiltes Secret. * * platform-plan.md §5.2: Der Hub exposet /.well-known/jwks.json. */ import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose"; import { getConfig } from "@/lib/config"; const JWKS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 Minuten (passend zum Hub-Cache-Header) interface CachedJwks { jwks: ReturnType; fetchedAt: number; } let cache: CachedJwks | null = null; function getHubUrl(): string { const hubUrl = getConfig().hubUrl; if (!hubUrl) { throw new Error("HUB_URL fehlt – Hub-SSO ist nicht konfiguriert."); } return hubUrl.replace(/\/$/, ""); } /** Remote-JWKSet mit einfachem TTL-Cache (jose hat keinen eingebauten). */ function getJwksSet(): ReturnType { const now = Date.now(); if (cache && now - cache.fetchedAt < JWKS_CACHE_TTL_MS) { return cache.jwks; } const jwks = createRemoteJWKSet(new URL(`${getHubUrl()}/.well-known/jwks.json`)); cache = { jwks, fetchedAt: now }; return jwks; } export interface HubTokenResult { hubId: string; email: string; name: string; isAdmin: boolean; } /** * Validiert ein Hub-Token (Signatur, TTL, aud, iss). * Wirft bei jedem Fehler – der Aufrufer entscheidet über die Response. */ export async function verifyHubToken(token: string): Promise { const config = getConfig(); const expectedAud = config.hubToolSlug; if (!expectedAud) { throw new Error("HUB_TOOL_SLUG fehlt – Tool-Slug für Hub-SSO nicht konfiguriert."); } const { payload } = await jwtVerify(token, getJwksSet(), { algorithms: ["RS256"], audience: expectedAud, issuer: config.hubIssuer ?? "http://localhost:3001", clockTolerance: 5, }); const hubId = payload.sub; const email = payload.email; const name = payload.name; if (typeof hubId !== "string" || typeof email !== "string" || typeof name !== "string") { throw new Error("Hub-Token enthält unvollständige Claims."); } return { hubId, email, name, isAdmin: payload.isAdmin === true, }; } /** Nur für Typ-Export (jose JWTPayload) – nicht Teil des Contracts. */ export type { JWTPayload };