CharlieBoyer HF Staff commited on
Commit
078bfcc
·
verified ·
1 Parent(s): 561d45d

Create src/server.ts

Browse files
Files changed (1) hide show
  1. src/server.ts +129 -0
src/server.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express, { Request, Response } from "express";
2
+
3
+ // Environment variables
4
+ const OKTA_AUTH = process.env.OKTA_EVENT_HOOK_AUTH || ""; // e.g., "Bearer my-shared-secret"
5
+ const FORWARD_URL = process.env.FORWARD_URL || ""; // external API endpoint you don't own
6
+ const FORWARD_AUTH = process.env.FORWARD_AUTH || ""; // optional auth for that API
7
+ const PORT = parseInt(process.env.PORT || "7860", 10); // HF Spaces usually route to 7860
8
+
9
+ // Minimal types for Okta event payloads
10
+ type OktaTarget = {
11
+ id?: string;
12
+ type?: string; // "User"
13
+ alternateId?: string; // usually the user's email
14
+ };
15
+
16
+ type OktaEvent = {
17
+ uuid?: string;
18
+ eventType?: string; // "user.lifecycle.delete"
19
+ published?: string; // ISO timestamp
20
+ target?: OktaTarget[];
21
+ };
22
+
23
+ type OktaEventHookBody = {
24
+ data?: { events?: OktaEvent[] };
25
+ };
26
+
27
+ // A tiny in-memory LRU-ish idempotency cache (swap for Redis/DB in prod)
28
+ class IdempotencyCache {
29
+ private maxSize: number;
30
+ private map = new Map<string, true>();
31
+ constructor(maxSize = 2000) { this.maxSize = maxSize; }
32
+ has(uuid: string) { return this.map.has(uuid); }
33
+ add(uuid: string) {
34
+ this.map.set(uuid, true);
35
+ if (this.map.size > this.maxSize) {
36
+ const firstKey = this.map.keys().next().value;
37
+ this.map.delete(firstKey);
38
+ }
39
+ }
40
+ }
41
+ const IDEMPOTENCY = new IdempotencyCache(2000);
42
+
43
+ const app = express();
44
+ app.disable("x-powered-by");
45
+ app.use(express.json({ limit: "256kb" }));
46
+
47
+ /**
48
+ * 1) One-time verification (Okta sends GET with x-okta-verification-challenge header)
49
+ */
50
+ app.get("/okta/events", (req: Request, res: Response) => {
51
+ const challenge = req.header("x-okta-verification-challenge");
52
+ if (!challenge) return res.status(400).send("Missing challenge");
53
+ return res.json({ verification: challenge });
54
+ });
55
+
56
+ /**
57
+ * 2) Event deliveries (POST). Respond fast (204); process in the background.
58
+ */
59
+ app.post("/okta/events", async (req: Request, res: Response) => {
60
+ if (OKTA_AUTH) {
61
+ const auth = req.header("authorization") || "";
62
+ if (auth !== OKTA_AUTH) {
63
+ return res.status(401).send("Unauthorized");
64
+ }
65
+ }
66
+
67
+ const body = (req.body || {}) as OktaEventHookBody;
68
+ const events = body.data?.events ?? [];
69
+
70
+ // Acknowledge immediately so Okta doesn't retry for slowness
71
+ res.status(204).end();
72
+
73
+ // Continue work asynchronously
74
+ setImmediate(() => processEvents(events).catch(console.error));
75
+ });
76
+
77
+ async function processEvents(events: OktaEvent[]) {
78
+ for (const evt of events) {
79
+ const uuid = evt.uuid;
80
+ if (!uuid) continue;
81
+ if (IDEMPOTENCY.has(uuid)) continue;
82
+ IDEMPOTENCY.add(uuid);
83
+
84
+ if (evt.eventType !== "user.lifecycle.delete") continue;
85
+
86
+ const user = (evt.target || []).find(t => t.type === "User");
87
+ const email = user?.alternateId;
88
+ if (!email) continue;
89
+
90
+ // Construct the payload expected by the external API (adjust as needed)
91
+ const payload = {
92
+ email,
93
+ eventTime: evt.published,
94
+ oktaUserId: user?.id,
95
+ eventId: uuid,
96
+ };
97
+
98
+ if (!FORWARD_URL) {
99
+ console.warn("FORWARD_URL not set; skipping forward:", payload);
100
+ continue;
101
+ }
102
+
103
+ try {
104
+ const headers: Record<string, string> = { "content-type": "application/json" };
105
+ if (FORWARD_AUTH) headers["authorization"] = FORWARD_AUTH;
106
+
107
+ const resp = await fetch(FORWARD_URL, {
108
+ method: "POST",
109
+ headers,
110
+ body: JSON.stringify(payload),
111
+ });
112
+
113
+ if (!resp.ok) {
114
+ console.error(`Forward failed: ${resp.status} ${resp.statusText}`);
115
+ }
116
+ } catch (err) {
117
+ console.error("Forward failed:", err);
118
+ // In production, enqueue for retry (e.g., SQS/Queue) rather than dropping it.
119
+ }
120
+ }
121
+ }
122
+
123
+ app.get("/", (_req, res) => {
124
+ res.type("text/plain").send("Okta Event Hook TS relay is running.");
125
+ });
126
+
127
+ app.listen(PORT, () => {
128
+ console.log(`Listening on http://0.0.0.0:${PORT}`);
129
+ });