import express, { Request, Response } from "express"; // Environment variables const OKTA_AUTH = process.env.OKTA_EVENT_HOOK_AUTH || ""; // e.g., "Bearer my-shared-secret" const FORWARD_URL = process.env.FORWARD_URL || ""; // external API endpoint you don't own const FORWARD_AUTH = process.env.FORWARD_AUTH || ""; // optional auth for that API const PORT = parseInt(process.env.PORT || "7860", 10); // HF Spaces usually route to 7860 // Minimal types for Okta event payloads type OktaTarget = { id?: string; type?: string; // "User" alternateId?: string; // usually the user's email }; type OktaEvent = { uuid?: string; eventType?: string; // "user.lifecycle.delete" published?: string; // ISO timestamp target?: OktaTarget[]; }; type OktaEventHookBody = { data?: { events?: OktaEvent[] }; }; // A tiny in-memory LRU-ish idempotency cache (swap for Redis/DB in prod) class IdempotencyCache { private maxSize: number; private map = new Map(); constructor(maxSize = 2000) { this.maxSize = maxSize; } has(uuid: string) { return this.map.has(uuid); } add(uuid: string) { this.map.set(uuid, true); if (this.map.size > this.maxSize) { const firstKey = this.map.keys().next().value as string | undefined; if (firstKey !== undefined) { this.map.delete(firstKey); } } } } const IDEMPOTENCY = new IdempotencyCache(2000); const app = express(); app.disable("x-powered-by"); app.use(express.json({ limit: "256kb" })); /** * 1) One-time verification (Okta sends GET with x-okta-verification-challenge header) */ app.get("/okta/events", (req: Request, res: Response) => { const challenge = req.header("x-okta-verification-challenge"); if (!challenge) return res.status(400).send("Missing challenge"); return res.json({ verification: challenge }); }); /** * 2) Event deliveries (POST). Respond fast (204); process in the background. */ app.post("/okta/events", async (req: Request, res: Response) => { if (OKTA_AUTH) { const auth = req.header("authorization") || ""; if (auth !== OKTA_AUTH) { return res.status(401).send("Unauthorized"); } } const body = (req.body || {}) as OktaEventHookBody; const events = body.data?.events ?? []; // Acknowledge immediately so Okta doesn't retry for slowness res.status(204).end(); // Continue work asynchronously setImmediate(() => processEvents(events).catch(console.error)); }); async function processEvents(events: OktaEvent[]) { for (const evt of events) { const uuid = evt.uuid; if (!uuid) continue; if (IDEMPOTENCY.has(uuid)) continue; IDEMPOTENCY.add(uuid); if (evt.eventType !== "user.lifecycle.delete") continue; const user = (evt.target || []).find(t => t.type === "User"); const email = user?.alternateId; if (!email) continue; // Construct the payload expected by the external API (adjust as needed) const payload = { email, eventTime: evt.published, oktaUserId: user?.id, eventId: uuid, }; if (!FORWARD_URL) { console.warn("FORWARD_URL not set; skipping forward:", payload); continue; } try { const headers: Record = { "content-type": "application/json" }; if (FORWARD_AUTH) headers["authorization"] = FORWARD_AUTH; const resp = await fetch(FORWARD_URL, { method: "POST", headers, body: JSON.stringify(payload), }); if (!resp.ok) { console.error(`Forward failed: ${resp.status} ${resp.statusText}`); } } catch (err) { console.error("Forward failed:", err); // In production, enqueue for retry (e.g., SQS/Queue) rather than dropping it. } } } app.get("/", (_req, res) => { res.type("text/plain").send("Okta Event Hook TS relay is running."); }); app.listen(PORT, () => { console.log(`Listening on http://0.0.0.0:${PORT}`); });