Spaces:
Sleeping
Sleeping
| 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<string, true>(); | |
| 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<string, string> = { "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}`); | |
| }); | |